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>
This commit is contained in:
Anton Sauchyk
2026-09-23 15:26:41 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent 7c6d626e1c
commit 1e2de43292
17 changed files with 2087 additions and 956 deletions
+57 -4
View File
@@ -154,13 +154,66 @@ is protoc — needed only to regenerate the `geyser_pb2` stubs in
`sys.path` and importing `src.geyser.generated`. Don't add a second copy under `sys.path` and importing `src.geyser.generated`. Don't add a second copy under
`cookbook/` — the last one drifted out of sync with the protos. `cookbook/` — the last one drifted out of sync with the protos.
The committed stubs are protobuf gencode **6.31.1** running against runtime The vendored protos are pinned to yellowstone **`v16.0.0-rc10+solana.4.3.0`**
7.36.2, which `ValidateProtobufRuntimeVersion` still accepts. Regenerating is (commit `301e9cf`, `yellowstone-grpc-proto` 13.0.0-rc4), which is the build the
not a plain `protoc` run: the committed files import each other absolutely endpoint itself reports from `GetVersion` — ask it before assuming, and pin to
what it answers. Not `master`: master has diverged and is *missing* things the
tag has (`block_footer`, `bank_id`, `VATDebit`). The committed stubs are
protobuf gencode **7.35.1** against runtime 7.36.2. Regenerating is not a plain
`protoc` run: the committed files import each other absolutely
(`from geyser.generated.solana_storage_pb2 import *`), which the compiler does (`from geyser.generated.solana_storage_pb2 import *`), which the compiler does
not emit on its own, so the output needs rewriting. Verified live under not emit on its own, so the output needs rewriting. Verified live under
grpcio 1.84 / protobuf 7.36.2 on 2026-09-23. grpcio 1.84 / protobuf 7.36.2 on 2026-09-23.
What the 13.0.0-rc4 protos changed, all verified live on 2026-09-23:
- **`Message.config` (field 7) is the only way to spot a transaction v1 over
geyser.** There is no version number on the wire and `versioned` is true for
v0 and v1 alike. The field is set only for v1 and carries the inline budget
(priority fee in **total lamports**, CU limit, loaded-accounts data size,
heap) that v1 moved off the ComputeBudget instructions — so a v1 coin has no
ComputeBudget instructions to read a budget from at all. Measured share of
pump.fun traffic: 660 of 4,215 transactions (15.7%) in a 90s window.
- Protos older than this **skipped `config` as an unknown field rather than
failing**, so the stale stubs degraded in silence: frames still decoded, only
the budget went missing. `verify_transaction_v1.py` now pins the geyser route
on its own committed frame so a rollback fails loudly.
- `TransactionStatusMeta.cost_units` and `Reward.commission_bps` are new too.
- **`CommitmentLevel` lost members 3-6**; they moved to a separate `SlotStatus`
enum. Nothing here sent anything but `PROCESSED`, so this cost nothing — but
it is the one breaking change in the upgrade.
- A yellowstone build **older than 15.1.1 silently downgrades v1 to v0 on the
wire**, which no local change can recover. Check `GetVersion` before
debugging a v1 gap.
`SubscribeDeshred` is a separate RPC delivering transactions **before
execution**, when entries form from shreds. It is **deliberately not wired into
the bot**, for two reasons, the second of which is disqualifying.
Raced against `Subscribe` on the pump.fun filter, 300s on 2026-09-23: identical
signature sets, no orphans either way. On creates deshred is first 92% of the
time, median **+6.4ms**, p90 +21.5ms; on general traffic 79%, median +4.0ms.
`tools/compare_deshred_latency.py` reproduces it — the far tail swings a lot
between runs, the median does not.
- There is no `TransactionStatusMeta`, so no `meta.log_messages`, so no
CreateEvent. That leaves `state_from_event` unset on every coin and silently
demotes `extreme_fast_mode` to a curve refresh.
- **A coin created through a router is invisible on it.** The create arrives as
a CPI, and inner instructions are produced *by* execution, so a pre-execution
stream never carries them. Measured 4 of 118 creates (3.4%) in one window and
3 of 101 in another — every one present on the stream and undetectable, not
dropped, so no decoding recovers them. Note this inverts the CPI note under
*Listener and decoder pitfalls*: **trades** are overwhelmingly inner
instructions, **creates** are overwhelmingly top-level (114:4), which is why
deshred looks nearly complete until you count.
Trading a permanent few-percent miss rate on new coins for a 6ms median is not
a trade this bot should take.
`cookbook/pumpfun/listen/pumpfun_listen_tokens_deshred.py` demonstrates the
stream and that trade-off; it decodes instructions because it has no logs to
route on.
Where the Solana libraries live, since solana-py 0.40 moved most of them Where the Solana libraries live, since solana-py 0.40 moved most of them
(upgraded 2026-09-23 from solders 0.26 / solana 0.36.6): (upgraded 2026-09-23 from solders 0.26 / solana 0.36.6):
@@ -189,7 +242,7 @@ name individual scripts to run a subset.
| `verify_v2_account_layout.py` | buy_v2/sell_v2 account layouts, PDA/ATA derivations, encoding — against `idl/pump_fun_idl.json` | | `verify_v2_account_layout.py` | buy_v2/sell_v2 account layouts, PDA/ATA derivations, encoding — against `idl/pump_fun_idl.json` |
| `verify_curve_account_sizes.py` | 125/151/256-byte curves all decode, and nothing filters on account length | | `verify_curve_account_sizes.py` | 125/151/256-byte curves all decode, and nothing filters on account length |
| `verify_create_v2_optional_args.py` | omitted trailing option-typed `create_v2` args decode as unset; mandatory args still fail | | `verify_create_v2_optional_args.py` | omitted trailing option-typed `create_v2` args decode as unset; mandatory args still fail |
| `verify_transaction_v1.py` | every reader asks `maxSupportedTransactionVersion: 1`, a v1 `create_v2` is detected from its logs alone with the envelope made unreadable, the envelope decode still works as the fallback, and no cookbook/tools listener detects by opening the envelope | | `verify_transaction_v1.py` | every reader asks `maxSupportedTransactionVersion: 1`, a v1 `create_v2` is detected from its logs alone with the envelope made unreadable, the envelope decode still works as the fallback, and no cookbook/tools listener detects by opening the envelope; same two routes pinned again over geyser, on a committed v1 `SubscribeUpdate`, plus the inline v1 budget staying readable |
| `verify_block_null_guard.py` | a `blockSubscribe` frame with `value.block: null` is skipped, not logged as an error | | `verify_block_null_guard.py` | a `blockSubscribe` frame with `value.block: null` is skipped, not logged as an error |
| `verify_listener_cancellation.py` | a cancelled WebSocket listener stops, even when `websockets` reports cancellation as `AssertionError` | | `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_pumpportal_buy_path.py` | curve derived from the mint, unreadable curve skips the buy, curve+mint read in one slot-consistent batch |
+4 -1
View File
@@ -90,12 +90,15 @@ Not specific to any of the launchpads below.
|---|---| |---|---|
| [`pumpfun/listen/pumpfun_listen_tokens_logsubscribe.py`](pumpfun/listen/pumpfun_listen_tokens_logsubscribe.py) | New coins over `logsSubscribe` — works on every provider | | [`pumpfun/listen/pumpfun_listen_tokens_logsubscribe.py`](pumpfun/listen/pumpfun_listen_tokens_logsubscribe.py) | New coins over `logsSubscribe` — works on every provider |
| [`pumpfun/listen/pumpfun_listen_tokens_blocksubscribe.py`](pumpfun/listen/pumpfun_listen_tokens_blocksubscribe.py) | New coins over `blockSubscribe` — whole blocks, slower, not on every provider | | [`pumpfun/listen/pumpfun_listen_tokens_blocksubscribe.py`](pumpfun/listen/pumpfun_listen_tokens_blocksubscribe.py) | New coins over `blockSubscribe` — whole blocks, slower, not on every provider |
| [`pumpfun/listen/pumpfun_listen_tokens_geyser.py`](pumpfun/listen/pumpfun_listen_tokens_geyser.py) | New coins over Geyser gRPC — the fastest of the four | | [`pumpfun/listen/pumpfun_listen_tokens_geyser.py`](pumpfun/listen/pumpfun_listen_tokens_geyser.py) | New coins over Geyser gRPC — the fastest of the four executed-stream listeners |
| [`pumpfun/listen/pumpfun_listen_tokens_deshred.py`](pumpfun/listen/pumpfun_listen_tokens_deshred.py) | New coins over Geyser `SubscribeDeshred` — ~6ms earlier, but pre-execution: no logs, no CreateEvent, no outcome, and router-created coins are invisible |
| [`pumpfun/listen/pumpfun_listen_tokens_pumpportal.py`](pumpfun/listen/pumpfun_listen_tokens_pumpportal.py) | New coins from PumpPortal's feed — third party, misses some coins | | [`pumpfun/listen/pumpfun_listen_tokens_pumpportal.py`](pumpfun/listen/pumpfun_listen_tokens_pumpportal.py) | New coins from PumpPortal's feed — third party, misses some coins |
| [`pumpfun/listen/pumpfun_listen_wallet_trades.py`](pumpfun/listen/pumpfun_listen_wallet_trades.py) | One wallet's bonding-curve buys and sells — copy trading | | [`pumpfun/listen/pumpfun_listen_wallet_trades.py`](pumpfun/listen/pumpfun_listen_wallet_trades.py) | One wallet's bonding-curve buys and sells — copy trading |
| [`pumpfun/listen/pumpfun_capture_transactions_blocksubscribe.py`](pumpfun/listen/pumpfun_capture_transactions_blocksubscribe.py) | Save live transactions to disk, to build a fixture | | [`pumpfun/listen/pumpfun_capture_transactions_blocksubscribe.py`](pumpfun/listen/pumpfun_capture_transactions_blocksubscribe.py) | Save live transactions to disk, to build a fixture |
Racing the four listeners against each other is `tools/compare_listeners.py`. Racing the four listeners against each other is `tools/compare_listeners.py`.
Racing the deshred stream against the executed one — and sizing its blind spot —
is `tools/compare_deshred_latency.py`.
### Read state ### Read state
File diff suppressed because one or more lines are too long
@@ -0,0 +1,305 @@
"""Monitors Solana for new Pump.fun token creations from the pre-execution deshred stream.
Decodes 'create' instructions the moment entries are formed from shreds, before the transaction runs.
Usage:
uv run cookbook/pumpfun/listen/pumpfun_listen_tokens_deshred.py
This is the companion to pumpfun_listen_tokens_geyser.py, which reads the ordinary
executed stream. The two are different RPCs on the same endpoint and the trade-off
between them is the point of this example.
`SubscribeDeshred` delivers a transaction when entries are formed from shreds,
BEFORE any execution. Measured head-to-head against `Subscribe` on the pump.fun
program, 300s on 2026-09-23: identical signature sets, no orphans either way.
On token creations -- the number that matters here -- deshred arrived first in
92% of cases, median +6.4ms, p75 +10.1ms, p90 +21.5ms. General traffic does
worse (79% first, median +4.0ms), and the far tail is volatile between runs.
`tools/compare_deshred_latency.py` reproduces all of this.
What you give up for those milliseconds:
- **It cannot see a coin created through a router, at all.** The create then
reaches the program as a CPI, and inner instructions are produced *by*
execution, so a pre-execution stream never carries them: the transaction
arrives on deshred with the create invisible. Measured 4 of 118 creates
(3.4%) in one 300s window and 3 of 101 in another -- every one of them
present on the stream and undetectable, not dropped. No decoding recovers
them, and this script misses them too. The executed listeners walk
`meta.innerInstructions` and do not have this blind spot.
- No TransactionStatusMeta, so no meta.log_messages, so no CreateEvent. This
script therefore decodes the create instruction, which is the fallback route
everywhere else in this repo. The creator it prints is the instruction's
`args.creator`, which is user-supplied and since 2026-04-28 may differ from
the canonical `BondingCurve.creator`. Read the curve if you need the real one.
- No success or failure, by construction -- nothing has executed yet. The
executed stream filters with `failed = False`; here you see every shredded
transaction, landed or reverted.
So this is a latency demo, not a better listener: a few milliseconds earlier on
most coins, in exchange for never seeing a few percent of them.
Address lookup tables are already resolved, reported as loaded_writable_addresses
then loaded_readonly_addresses (that order), so instruction account indices
resolve the same way they do on the executed stream.
Geyser gRPC Reference:
https://docs.triton.one/project-yellowstone/dragons-mouth-grpc-subscriptions
Authentication: Supports both Basic and X-Token authentication methods.
Configure via GEYSER_ENDPOINT, GEYSER_API_TOKEN, and AUTH_TYPE variables.
"""
import asyncio
import os
import struct
import sys
from pathlib import Path
import base58
import grpc
from dotenv import load_dotenv
from solders.pubkey import Pubkey
# The geyser stubs are generated once, into src/geyser/generated. Reuse them rather
# than keeping a second copy here that drifts out of sync with proto/.
sys.path.insert(0, str(Path(__file__).resolve().parents[3]))
from src.geyser.generated import geyser_pb2, geyser_pb2_grpc
load_dotenv()
GEYSER_ENDPOINT = os.getenv("GEYSER_ENDPOINT")
GEYSER_API_TOKEN = os.getenv("GEYSER_API_TOKEN")
# Authentication type: "x-token" or "basic"
AUTH_TYPE = "x-token"
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
# Instruction discriminators (8-byte identifiers for instruction types)
# Calculated using the first 8 bytes of sha256("global:create") for legacy Create
# and sha256("global:createV2") for Token2022 CreateV2
# Run cookbook/solana/anchor_calculate_discriminator.py to recompute them.
PUMP_CREATE_PREFIX = struct.pack("<Q", 8576854823835016728)
PUMP_CREATE_V2_PREFIX = bytes([214, 144, 76, 236, 95, 139, 49, 180])
def print_token_info(token_data: dict, signature: str, slot: int) -> None:
"""Print token information in a consistent, user-friendly format.
Args:
token_data: Dictionary containing token fields
signature: Transaction signature
slot: Slot the entry was shredded in
"""
print("\n" + "=" * 80)
print("🎯 NEW TOKEN DETECTED (pre-execution)")
print("=" * 80)
print(f"Name: {token_data.get('name', 'N/A')}")
print(f"Symbol: {token_data.get('symbol', 'N/A')}")
print(f"Mint: {token_data.get('mint', 'N/A')}")
print(f"Bonding Curve: {token_data.get('bonding_curve', 'N/A')}")
print(f"Associated BC: {token_data.get('associated_bonding_curve', 'N/A')}")
print(f"User: {token_data.get('user', 'N/A')}")
# From the instruction args, not the curve: may differ from BondingCurve.creator.
print(f"Creator (args): {token_data.get('creator', 'N/A')}")
print(f"Token Standard: {token_data.get('token_standard', 'N/A')}")
print(f"Mayhem Mode: {token_data.get('is_mayhem_mode', False)}")
print(f"URI: {token_data.get('uri', 'N/A')}")
print(f"Tx version: {token_data.get('tx_version', 'N/A')}")
print(f"Slot: {slot}")
print(f"Signature: {signature}")
print("Outcome: unknown - nothing has executed yet")
print("=" * 80 + "\n")
async def create_geyser_connection() -> geyser_pb2_grpc.GeyserStub:
"""Establish a secure connection to the Geyser endpoint using the configured auth type.
Returns:
A Geyser stub bound to an authenticated channel
"""
if AUTH_TYPE == "x-token":
auth = grpc.metadata_call_credentials(
lambda _, callback: callback((("x-token", GEYSER_API_TOKEN),), None)
)
else: # Default to basic auth
auth = grpc.metadata_call_credentials(
lambda _, callback: callback(
(("authorization", f"Basic {GEYSER_API_TOKEN}"),), None
)
)
creds = grpc.composite_channel_credentials(grpc.ssl_channel_credentials(), auth)
channel = grpc.aio.secure_channel(GEYSER_ENDPOINT, creds)
return geyser_pb2_grpc.GeyserStub(channel)
def create_subscription_request() -> geyser_pb2.SubscribeDeshredRequest:
"""Create a deshred subscription request for Pump.fun transactions.
This is its own request type, not a SubscribeRequest: the deshred stream has
no commitment level, because commitment describes execution and nothing has
executed. There is also no `failed` filter for the same reason.
Returns:
The request to open the stream with
"""
request = geyser_pb2.SubscribeDeshredRequest()
request.deshred_transactions["pump_filter"].account_include.append(
str(PUMP_PROGRAM_ID)
)
request.deshred_transactions["pump_filter"].vote = False
return request
def resolve_account_keys(
tx: geyser_pb2.SubscribeUpdateDeshredTransactionInfo,
) -> list[bytes]:
"""Build the full account-key table for one deshred transaction.
A v0 transaction indexes accounts past the end of `message.account_keys` when
it uses an address lookup table. The deshred stream resolves those for you and
reports them on the update itself rather than under a meta, in this exact
order: static keys, then writable loaded, then read-only loaded.
Args:
tx: A geyser `SubscribeUpdateDeshredTransactionInfo`
Returns:
Account keys as raw 32-byte values, indexable by an instruction's account list
"""
keys = list(tx.transaction.message.account_keys)
keys.extend(tx.loaded_writable_addresses)
keys.extend(tx.loaded_readonly_addresses)
return keys
def decode_create_instruction(
ix_data: bytes, keys: list[bytes], accounts: bytes, *, is_v2: bool
) -> dict:
"""Decode a create or create_v2 instruction from its raw data.
Args:
ix_data: Instruction data, starting with the 8-byte discriminator
keys: Full account-key table from `resolve_account_keys`
accounts: The instruction's account indices
is_v2: True for `create_v2` (Token2022), False for legacy `create`
Returns:
Dictionary of decoded token fields
"""
offset = 8 # Skip past the 8-byte discriminator prefix
def get_account_key(index: int) -> str:
if index >= len(accounts):
return "N/A"
account_index = accounts[index]
if account_index >= len(keys):
return "N/A"
return base58.b58encode(keys[account_index]).decode()
def read_string() -> str:
nonlocal offset
length = struct.unpack_from("<I", ix_data, offset)[0]
offset += 4
value = ix_data[offset : offset + length].decode()
offset += length
return value
def read_pubkey() -> str:
nonlocal offset
value = base58.b58encode(ix_data[offset : offset + 32]).decode("utf-8")
offset += 32
return value
name = read_string()
symbol = read_string()
uri = read_string()
creator = read_pubkey()
if not is_v2:
return {
"name": name,
"symbol": symbol,
"uri": uri,
"creator": creator,
"mint": get_account_key(0),
"bonding_curve": get_account_key(2),
"associated_bonding_curve": get_account_key(3),
"user": get_account_key(7),
"token_standard": "legacy",
"is_mayhem_mode": False,
}
# create_v2 trailing args are positional and may be truncated on the wire:
# read defensively and report a missing one as unset rather than raising.
is_mayhem_mode = False
if offset < len(ix_data):
is_mayhem_mode = bool(ix_data[offset])
offset += 1
return {
"name": name,
"symbol": symbol,
"uri": uri,
"creator": creator,
"mint": get_account_key(0),
"bonding_curve": get_account_key(2),
"associated_bonding_curve": get_account_key(3),
"user": get_account_key(5),
"token_standard": "token2022",
"is_mayhem_mode": is_mayhem_mode,
}
def describe_version(message: geyser_pb2.Message) -> str:
"""Name the transaction format a message is in.
`config` is set only for transaction v1 (SIMD-0385), so its presence is what
separates v1 from v0 -- the `versioned` flag is true for both.
Args:
message: The transaction's message
Returns:
"v1", "v0" or "legacy"
"""
if message.HasField("config"):
return "v1"
return "v0" if message.versioned else "legacy"
async def monitor_pump() -> None:
"""Monitor the deshred stream for new Pump.fun token creations."""
print(f"Starting Pump.fun deshred monitor using {AUTH_TYPE.upper()} authentication")
print("Reading pre-execution transactions: outcomes are not yet known.\n")
stub = await create_geyser_connection()
request = create_subscription_request()
async for update in stub.SubscribeDeshred(iter([request])):
if not update.HasField("deshred_transaction"):
continue
tx = update.deshred_transaction.transaction
msg = getattr(tx.transaction, "message", None)
if msg is None:
continue
keys = resolve_account_keys(tx)
for ix in msg.instructions:
is_create = ix.data.startswith(PUMP_CREATE_PREFIX)
is_create_v2 = ix.data.startswith(PUMP_CREATE_V2_PREFIX)
if not (is_create or is_create_v2):
continue
info = decode_create_instruction(
ix.data, keys, ix.accounts, is_v2=is_create_v2
)
info["tx_version"] = describe_version(msg)
signature = base58.b58encode(bytes(tx.signature)).decode()
print_token_info(info, signature, update.deshred_transaction.slot)
if __name__ == "__main__":
asyncio.run(monitor_pump())
@@ -1,5 +1,6 @@
"""Monitors Solana for new Pump.fun token creations using Geyser gRPC. """Monitors Solana for new Pump.fun token creations using Geyser gRPC.
Decodes 'create' instructions to extract and display token details (name, symbol, mint, bonding curve). Decodes 'create' instructions to extract and display token details (name, symbol, mint, bonding curve),
and reports which transaction format each coin was created in, with its inline budget for v1.
Usage: Usage:
uv run cookbook/pumpfun/listen/pumpfun_listen_tokens_geyser.py uv run cookbook/pumpfun/listen/pumpfun_listen_tokens_geyser.py
@@ -51,13 +52,14 @@ PUMP_CREATE_PREFIX = struct.pack("<Q", 8576854823835016728)
PUMP_CREATE_V2_PREFIX = bytes([214, 144, 76, 236, 95, 139, 49, 180]) PUMP_CREATE_V2_PREFIX = bytes([214, 144, 76, 236, 95, 139, 49, 180])
def print_token_info(token_data, signature=None): def print_token_info(token_data, signature=None, envelope: dict | None = None):
""" """
Print token information in a consistent, user-friendly format. Print token information in a consistent, user-friendly format.
Args: Args:
token_data: Dictionary containing token fields token_data: Dictionary containing token fields
signature: Optional transaction signature signature: Optional transaction signature
envelope: Optional envelope summary from `describe_envelope`
""" """
print("\n" + "=" * 80) print("\n" + "=" * 80)
print("🎯 NEW TOKEN DETECTED") print("🎯 NEW TOKEN DETECTED")
@@ -82,6 +84,12 @@ def print_token_info(token_data, signature=None):
print(f"URI: {token_data['uri']}") print(f"URI: {token_data['uri']}")
if signature: if signature:
print(f"Signature: {signature}") print(f"Signature: {signature}")
if envelope:
print(f"Tx version: {envelope['version']}")
if envelope["config"]:
print(f"Tx config: {envelope['config']}")
if envelope["cost_units"] is not None:
print(f"Cost units: {envelope['cost_units']}")
print("=" * 80 + "\n") print("=" * 80 + "\n")
@@ -138,6 +146,55 @@ def resolve_account_keys(
return keys return keys
def describe_envelope(tx: geyser_pb2.SubscribeUpdateTransactionInfo) -> dict:
"""Summarise which transaction format a coin was created in, and its budget.
Transaction v1 (SIMD-0385, live since 2026-09-15) carries its compute budget
inline on the message as `config`, instead of as separate ComputeBudget
instructions. Geyser sets that field only for v1, so its presence is how you
tell a v1 transaction from a legacy or v0 one here: the `versioned` flag is
true for both v0 and v1 and cannot separate them.
Args:
tx: A geyser `SubscribeUpdateTransactionInfo`
Returns:
Dict with the detected `version`, a readable `config` string (empty for
legacy/v0) and `cost_units` when the validator reported it
"""
msg = tx.transaction.message
is_v1 = msg.HasField("config")
config = ""
if is_v1:
cfg = msg.config
parts = []
# Every field is optional, and an unset one means zero rather than a
# default: a v1 transaction that omits the compute unit limit budgets
# 0 CU and dies at account loading.
if cfg.HasField("priority_fee"):
# v1 states a total in lamports, not micro-lamports per CU as v0 does.
parts.append(f"priority_fee={cfg.priority_fee} lamports")
if cfg.HasField("compute_unit_limit"):
parts.append(f"cu_limit={cfg.compute_unit_limit}")
if cfg.HasField("loaded_accounts_data_size_limit"):
parts.append(f"data_size={cfg.loaded_accounts_data_size_limit}")
if cfg.HasField("heap_size"):
parts.append(f"heap={cfg.heap_size}")
config = ", ".join(parts) or "all fields unset"
meta = getattr(tx, "meta", None)
cost_units = None
if meta is not None and meta.HasField("cost_units"):
cost_units = meta.cost_units
return {
"version": "v1" if is_v1 else ("v0" if msg.versioned else "legacy"),
"config": config,
"cost_units": cost_units,
}
def decode_create_instruction(ix_data: bytes, keys, accounts) -> dict: def decode_create_instruction(ix_data: bytes, keys, accounts) -> dict:
"""Decode a legacy create instruction (Metaplex) from transaction data.""" """Decode a legacy create instruction (Metaplex) from transaction data."""
# Skip past the 8-byte discriminator prefix # Skip past the 8-byte discriminator prefix
@@ -303,7 +360,11 @@ async def monitor_pump():
).decode() ).decode()
# Print token information in consistent format # Print token information in consistent format
print_token_info(info, signature=signature) print_token_info(
info,
signature=signature,
envelope=describe_envelope(update.transaction.transaction),
)
if __name__ == "__main__": if __name__ == "__main__":
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+301 -204
View File
@@ -1,30 +1,27 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services.""" """Client and server classes corresponding to protobuf-defined services."""
import grpc import grpc
import warnings
import geyser.generated.geyser_pb2 as geyser__pb2 import geyser.generated.geyser_pb2 as geyser__pb2
GRPC_GENERATED_VERSION = "1.71.0" GRPC_GENERATED_VERSION = '1.84.0'
GRPC_VERSION = grpc.__version__ GRPC_VERSION = grpc.__version__
_version_not_supported = False _version_not_supported = False
try: try:
from grpc._utilities import first_version_is_lower from grpc._utilities import first_version_is_lower
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
_version_not_supported = first_version_is_lower(
GRPC_VERSION, GRPC_GENERATED_VERSION
)
except ImportError: except ImportError:
_version_not_supported = True _version_not_supported = True
if _version_not_supported: if _version_not_supported:
raise RuntimeError( raise RuntimeError(
f"The grpc package installed is at version {GRPC_VERSION}," f'The grpc package installed is at version {GRPC_VERSION},'
+ " but the generated code in geyser_pb2_grpc.py depends on" + ' but the generated code in geyser_pb2_grpc.py depends on'
+ f" grpcio>={GRPC_GENERATED_VERSION}." + f' grpcio>={GRPC_GENERATED_VERSION}.'
+ f" Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}" + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ f" or downgrade your generated code using grpcio-tools<={GRPC_VERSION}." + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
) )
@@ -38,47 +35,55 @@ class GeyserStub:
channel: A grpc.Channel. channel: A grpc.Channel.
""" """
self.Subscribe = channel.stream_stream( self.Subscribe = channel.stream_stream(
"/geyser.Geyser/Subscribe", '/geyser.Geyser/Subscribe',
request_serializer=geyser__pb2.SubscribeRequest.SerializeToString, request_serializer=geyser__pb2.SubscribeRequest.SerializeToString,
response_deserializer=geyser__pb2.SubscribeUpdate.FromString, response_deserializer=geyser__pb2.SubscribeUpdate.FromString,
_registered_method=True, _registered_method=True)
) self.SubscribeDeshred = channel.stream_stream(
'/geyser.Geyser/SubscribeDeshred',
request_serializer=geyser__pb2.SubscribeDeshredRequest.SerializeToString,
response_deserializer=geyser__pb2.SubscribeUpdateDeshred.FromString,
_registered_method=True)
self.SubscribeGossip = channel.unary_stream(
'/geyser.Geyser/SubscribeGossip',
request_serializer=geyser__pb2.SubscribeGossipRequest.SerializeToString,
response_deserializer=geyser__pb2.SubscribeUpdateGossip.FromString,
_registered_method=True)
self.SubscribeReplayInfo = channel.unary_unary(
'/geyser.Geyser/SubscribeReplayInfo',
request_serializer=geyser__pb2.SubscribeReplayInfoRequest.SerializeToString,
response_deserializer=geyser__pb2.SubscribeReplayInfoResponse.FromString,
_registered_method=True)
self.Ping = channel.unary_unary( self.Ping = channel.unary_unary(
"/geyser.Geyser/Ping", '/geyser.Geyser/Ping',
request_serializer=geyser__pb2.PingRequest.SerializeToString, request_serializer=geyser__pb2.PingRequest.SerializeToString,
response_deserializer=geyser__pb2.PongResponse.FromString, response_deserializer=geyser__pb2.PongResponse.FromString,
_registered_method=True, _registered_method=True)
)
self.GetLatestBlockhash = channel.unary_unary( self.GetLatestBlockhash = channel.unary_unary(
"/geyser.Geyser/GetLatestBlockhash", '/geyser.Geyser/GetLatestBlockhash',
request_serializer=geyser__pb2.GetLatestBlockhashRequest.SerializeToString, request_serializer=geyser__pb2.GetLatestBlockhashRequest.SerializeToString,
response_deserializer=geyser__pb2.GetLatestBlockhashResponse.FromString, response_deserializer=geyser__pb2.GetLatestBlockhashResponse.FromString,
_registered_method=True, _registered_method=True)
)
self.GetBlockHeight = channel.unary_unary( self.GetBlockHeight = channel.unary_unary(
"/geyser.Geyser/GetBlockHeight", '/geyser.Geyser/GetBlockHeight',
request_serializer=geyser__pb2.GetBlockHeightRequest.SerializeToString, request_serializer=geyser__pb2.GetBlockHeightRequest.SerializeToString,
response_deserializer=geyser__pb2.GetBlockHeightResponse.FromString, response_deserializer=geyser__pb2.GetBlockHeightResponse.FromString,
_registered_method=True, _registered_method=True)
)
self.GetSlot = channel.unary_unary( self.GetSlot = channel.unary_unary(
"/geyser.Geyser/GetSlot", '/geyser.Geyser/GetSlot',
request_serializer=geyser__pb2.GetSlotRequest.SerializeToString, request_serializer=geyser__pb2.GetSlotRequest.SerializeToString,
response_deserializer=geyser__pb2.GetSlotResponse.FromString, response_deserializer=geyser__pb2.GetSlotResponse.FromString,
_registered_method=True, _registered_method=True)
)
self.IsBlockhashValid = channel.unary_unary( self.IsBlockhashValid = channel.unary_unary(
"/geyser.Geyser/IsBlockhashValid", '/geyser.Geyser/IsBlockhashValid',
request_serializer=geyser__pb2.IsBlockhashValidRequest.SerializeToString, request_serializer=geyser__pb2.IsBlockhashValidRequest.SerializeToString,
response_deserializer=geyser__pb2.IsBlockhashValidResponse.FromString, response_deserializer=geyser__pb2.IsBlockhashValidResponse.FromString,
_registered_method=True, _registered_method=True)
)
self.GetVersion = channel.unary_unary( self.GetVersion = channel.unary_unary(
"/geyser.Geyser/GetVersion", '/geyser.Geyser/GetVersion',
request_serializer=geyser__pb2.GetVersionRequest.SerializeToString, request_serializer=geyser__pb2.GetVersionRequest.SerializeToString,
response_deserializer=geyser__pb2.GetVersionResponse.FromString, response_deserializer=geyser__pb2.GetVersionResponse.FromString,
_registered_method=True, _registered_method=True)
)
class GeyserServicer: class GeyserServicer:
@@ -87,112 +92,142 @@ class GeyserServicer:
def Subscribe(self, request_iterator, context): def Subscribe(self, request_iterator, context):
"""Missing associated documentation comment in .proto file.""" """Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!") context.set_details('Method not implemented!')
raise NotImplementedError("Method not implemented!") raise NotImplementedError('Method not implemented!')
def SubscribeDeshred(self, request_iterator, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def SubscribeGossip(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def SubscribeReplayInfo(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
def Ping(self, request, context): def Ping(self, request, context):
"""Missing associated documentation comment in .proto file.""" """Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!") context.set_details('Method not implemented!')
raise NotImplementedError("Method not implemented!") raise NotImplementedError('Method not implemented!')
def GetLatestBlockhash(self, request, context): def GetLatestBlockhash(self, request, context):
"""Missing associated documentation comment in .proto file.""" """Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!") context.set_details('Method not implemented!')
raise NotImplementedError("Method not implemented!") raise NotImplementedError('Method not implemented!')
def GetBlockHeight(self, request, context): def GetBlockHeight(self, request, context):
"""Missing associated documentation comment in .proto file.""" """Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!") context.set_details('Method not implemented!')
raise NotImplementedError("Method not implemented!") raise NotImplementedError('Method not implemented!')
def GetSlot(self, request, context): def GetSlot(self, request, context):
"""Missing associated documentation comment in .proto file.""" """Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!") context.set_details('Method not implemented!')
raise NotImplementedError("Method not implemented!") raise NotImplementedError('Method not implemented!')
def IsBlockhashValid(self, request, context): def IsBlockhashValid(self, request, context):
"""Missing associated documentation comment in .proto file.""" """Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!") context.set_details('Method not implemented!')
raise NotImplementedError("Method not implemented!") raise NotImplementedError('Method not implemented!')
def GetVersion(self, request, context): def GetVersion(self, request, context):
"""Missing associated documentation comment in .proto file.""" """Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details("Method not implemented!") context.set_details('Method not implemented!')
raise NotImplementedError("Method not implemented!") raise NotImplementedError('Method not implemented!')
def add_GeyserServicer_to_server(servicer, server): def add_GeyserServicer_to_server(servicer, server):
rpc_method_handlers = { rpc_method_handlers = {
"Subscribe": grpc.stream_stream_rpc_method_handler( 'Subscribe': grpc.stream_stream_rpc_method_handler(
servicer.Subscribe, servicer.Subscribe,
request_deserializer=geyser__pb2.SubscribeRequest.FromString, request_deserializer=geyser__pb2.SubscribeRequest.FromString,
response_serializer=geyser__pb2.SubscribeUpdate.SerializeToString, response_serializer=geyser__pb2.SubscribeUpdate.SerializeToString,
), ),
"Ping": grpc.unary_unary_rpc_method_handler( 'SubscribeDeshred': grpc.stream_stream_rpc_method_handler(
servicer.Ping, servicer.SubscribeDeshred,
request_deserializer=geyser__pb2.PingRequest.FromString, request_deserializer=geyser__pb2.SubscribeDeshredRequest.FromString,
response_serializer=geyser__pb2.PongResponse.SerializeToString, response_serializer=geyser__pb2.SubscribeUpdateDeshred.SerializeToString,
), ),
"GetLatestBlockhash": grpc.unary_unary_rpc_method_handler( 'SubscribeGossip': grpc.unary_stream_rpc_method_handler(
servicer.GetLatestBlockhash, servicer.SubscribeGossip,
request_deserializer=geyser__pb2.GetLatestBlockhashRequest.FromString, request_deserializer=geyser__pb2.SubscribeGossipRequest.FromString,
response_serializer=geyser__pb2.GetLatestBlockhashResponse.SerializeToString, response_serializer=geyser__pb2.SubscribeUpdateGossip.SerializeToString,
), ),
"GetBlockHeight": grpc.unary_unary_rpc_method_handler( 'SubscribeReplayInfo': grpc.unary_unary_rpc_method_handler(
servicer.GetBlockHeight, servicer.SubscribeReplayInfo,
request_deserializer=geyser__pb2.GetBlockHeightRequest.FromString, request_deserializer=geyser__pb2.SubscribeReplayInfoRequest.FromString,
response_serializer=geyser__pb2.GetBlockHeightResponse.SerializeToString, response_serializer=geyser__pb2.SubscribeReplayInfoResponse.SerializeToString,
), ),
"GetSlot": grpc.unary_unary_rpc_method_handler( 'Ping': grpc.unary_unary_rpc_method_handler(
servicer.GetSlot, servicer.Ping,
request_deserializer=geyser__pb2.GetSlotRequest.FromString, request_deserializer=geyser__pb2.PingRequest.FromString,
response_serializer=geyser__pb2.GetSlotResponse.SerializeToString, response_serializer=geyser__pb2.PongResponse.SerializeToString,
), ),
"IsBlockhashValid": grpc.unary_unary_rpc_method_handler( 'GetLatestBlockhash': grpc.unary_unary_rpc_method_handler(
servicer.IsBlockhashValid, servicer.GetLatestBlockhash,
request_deserializer=geyser__pb2.IsBlockhashValidRequest.FromString, request_deserializer=geyser__pb2.GetLatestBlockhashRequest.FromString,
response_serializer=geyser__pb2.IsBlockhashValidResponse.SerializeToString, response_serializer=geyser__pb2.GetLatestBlockhashResponse.SerializeToString,
), ),
"GetVersion": grpc.unary_unary_rpc_method_handler( 'GetBlockHeight': grpc.unary_unary_rpc_method_handler(
servicer.GetVersion, servicer.GetBlockHeight,
request_deserializer=geyser__pb2.GetVersionRequest.FromString, request_deserializer=geyser__pb2.GetBlockHeightRequest.FromString,
response_serializer=geyser__pb2.GetVersionResponse.SerializeToString, response_serializer=geyser__pb2.GetBlockHeightResponse.SerializeToString,
), ),
'GetSlot': grpc.unary_unary_rpc_method_handler(
servicer.GetSlot,
request_deserializer=geyser__pb2.GetSlotRequest.FromString,
response_serializer=geyser__pb2.GetSlotResponse.SerializeToString,
),
'IsBlockhashValid': grpc.unary_unary_rpc_method_handler(
servicer.IsBlockhashValid,
request_deserializer=geyser__pb2.IsBlockhashValidRequest.FromString,
response_serializer=geyser__pb2.IsBlockhashValidResponse.SerializeToString,
),
'GetVersion': grpc.unary_unary_rpc_method_handler(
servicer.GetVersion,
request_deserializer=geyser__pb2.GetVersionRequest.FromString,
response_serializer=geyser__pb2.GetVersionResponse.SerializeToString,
),
} }
generic_handler = grpc.method_handlers_generic_handler( generic_handler = grpc.method_handlers_generic_handler(
"geyser.Geyser", rpc_method_handlers 'geyser.Geyser', rpc_method_handlers)
)
server.add_generic_rpc_handlers((generic_handler,)) server.add_generic_rpc_handlers((generic_handler,))
server.add_registered_method_handlers("geyser.Geyser", rpc_method_handlers) server.add_registered_method_handlers('geyser.Geyser', rpc_method_handlers)
# This class is part of an EXPERIMENTAL API. # This class is part of an EXPERIMENTAL API.
class Geyser: class Geyser:
"""Missing associated documentation comment in .proto file.""" """Missing associated documentation comment in .proto file."""
@staticmethod @staticmethod
def Subscribe( def Subscribe(request_iterator,
request_iterator, target,
target, options=(),
options=(), channel_credentials=None,
channel_credentials=None, call_credentials=None,
call_credentials=None, insecure=False,
insecure=False, compression=None,
compression=None, wait_for_ready=None,
wait_for_ready=None, timeout=None,
timeout=None, metadata=None):
metadata=None,
):
return grpc.experimental.stream_stream( return grpc.experimental.stream_stream(
request_iterator, request_iterator,
target, target,
"/geyser.Geyser/Subscribe", '/geyser.Geyser/Subscribe',
geyser__pb2.SubscribeRequest.SerializeToString, geyser__pb2.SubscribeRequest.SerializeToString,
geyser__pb2.SubscribeUpdate.FromString, geyser__pb2.SubscribeUpdate.FromString,
options, options,
@@ -203,26 +238,104 @@ class Geyser:
wait_for_ready, wait_for_ready,
timeout, timeout,
metadata, metadata,
_registered_method=True, _registered_method=True)
)
@staticmethod @staticmethod
def Ping( def SubscribeDeshred(request_iterator,
request, target,
target, options=(),
options=(), channel_credentials=None,
channel_credentials=None, call_credentials=None,
call_credentials=None, insecure=False,
insecure=False, compression=None,
compression=None, wait_for_ready=None,
wait_for_ready=None, timeout=None,
timeout=None, metadata=None):
metadata=None, return grpc.experimental.stream_stream(
): request_iterator,
target,
'/geyser.Geyser/SubscribeDeshred',
geyser__pb2.SubscribeDeshredRequest.SerializeToString,
geyser__pb2.SubscribeUpdateDeshred.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def SubscribeGossip(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_stream(
request,
target,
'/geyser.Geyser/SubscribeGossip',
geyser__pb2.SubscribeGossipRequest.SerializeToString,
geyser__pb2.SubscribeUpdateGossip.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def SubscribeReplayInfo(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary( return grpc.experimental.unary_unary(
request, request,
target, target,
"/geyser.Geyser/Ping", '/geyser.Geyser/SubscribeReplayInfo',
geyser__pb2.SubscribeReplayInfoRequest.SerializeToString,
geyser__pb2.SubscribeReplayInfoResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def Ping(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(
request,
target,
'/geyser.Geyser/Ping',
geyser__pb2.PingRequest.SerializeToString, geyser__pb2.PingRequest.SerializeToString,
geyser__pb2.PongResponse.FromString, geyser__pb2.PongResponse.FromString,
options, options,
@@ -233,26 +346,23 @@ class Geyser:
wait_for_ready, wait_for_ready,
timeout, timeout,
metadata, metadata,
_registered_method=True, _registered_method=True)
)
@staticmethod @staticmethod
def GetLatestBlockhash( def GetLatestBlockhash(request,
request, target,
target, options=(),
options=(), channel_credentials=None,
channel_credentials=None, call_credentials=None,
call_credentials=None, insecure=False,
insecure=False, compression=None,
compression=None, wait_for_ready=None,
wait_for_ready=None, timeout=None,
timeout=None, metadata=None):
metadata=None,
):
return grpc.experimental.unary_unary( return grpc.experimental.unary_unary(
request, request,
target, target,
"/geyser.Geyser/GetLatestBlockhash", '/geyser.Geyser/GetLatestBlockhash',
geyser__pb2.GetLatestBlockhashRequest.SerializeToString, geyser__pb2.GetLatestBlockhashRequest.SerializeToString,
geyser__pb2.GetLatestBlockhashResponse.FromString, geyser__pb2.GetLatestBlockhashResponse.FromString,
options, options,
@@ -263,26 +373,23 @@ class Geyser:
wait_for_ready, wait_for_ready,
timeout, timeout,
metadata, metadata,
_registered_method=True, _registered_method=True)
)
@staticmethod @staticmethod
def GetBlockHeight( def GetBlockHeight(request,
request, target,
target, options=(),
options=(), channel_credentials=None,
channel_credentials=None, call_credentials=None,
call_credentials=None, insecure=False,
insecure=False, compression=None,
compression=None, wait_for_ready=None,
wait_for_ready=None, timeout=None,
timeout=None, metadata=None):
metadata=None,
):
return grpc.experimental.unary_unary( return grpc.experimental.unary_unary(
request, request,
target, target,
"/geyser.Geyser/GetBlockHeight", '/geyser.Geyser/GetBlockHeight',
geyser__pb2.GetBlockHeightRequest.SerializeToString, geyser__pb2.GetBlockHeightRequest.SerializeToString,
geyser__pb2.GetBlockHeightResponse.FromString, geyser__pb2.GetBlockHeightResponse.FromString,
options, options,
@@ -293,26 +400,23 @@ class Geyser:
wait_for_ready, wait_for_ready,
timeout, timeout,
metadata, metadata,
_registered_method=True, _registered_method=True)
)
@staticmethod @staticmethod
def GetSlot( def GetSlot(request,
request, target,
target, options=(),
options=(), channel_credentials=None,
channel_credentials=None, call_credentials=None,
call_credentials=None, insecure=False,
insecure=False, compression=None,
compression=None, wait_for_ready=None,
wait_for_ready=None, timeout=None,
timeout=None, metadata=None):
metadata=None,
):
return grpc.experimental.unary_unary( return grpc.experimental.unary_unary(
request, request,
target, target,
"/geyser.Geyser/GetSlot", '/geyser.Geyser/GetSlot',
geyser__pb2.GetSlotRequest.SerializeToString, geyser__pb2.GetSlotRequest.SerializeToString,
geyser__pb2.GetSlotResponse.FromString, geyser__pb2.GetSlotResponse.FromString,
options, options,
@@ -323,26 +427,23 @@ class Geyser:
wait_for_ready, wait_for_ready,
timeout, timeout,
metadata, metadata,
_registered_method=True, _registered_method=True)
)
@staticmethod @staticmethod
def IsBlockhashValid( def IsBlockhashValid(request,
request, target,
target, options=(),
options=(), channel_credentials=None,
channel_credentials=None, call_credentials=None,
call_credentials=None, insecure=False,
insecure=False, compression=None,
compression=None, wait_for_ready=None,
wait_for_ready=None, timeout=None,
timeout=None, metadata=None):
metadata=None,
):
return grpc.experimental.unary_unary( return grpc.experimental.unary_unary(
request, request,
target, target,
"/geyser.Geyser/IsBlockhashValid", '/geyser.Geyser/IsBlockhashValid',
geyser__pb2.IsBlockhashValidRequest.SerializeToString, geyser__pb2.IsBlockhashValidRequest.SerializeToString,
geyser__pb2.IsBlockhashValidResponse.FromString, geyser__pb2.IsBlockhashValidResponse.FromString,
options, options,
@@ -353,26 +454,23 @@ class Geyser:
wait_for_ready, wait_for_ready,
timeout, timeout,
metadata, metadata,
_registered_method=True, _registered_method=True)
)
@staticmethod @staticmethod
def GetVersion( def GetVersion(request,
request, target,
target, options=(),
options=(), channel_credentials=None,
channel_credentials=None, call_credentials=None,
call_credentials=None, insecure=False,
insecure=False, compression=None,
compression=None, wait_for_ready=None,
wait_for_ready=None, timeout=None,
timeout=None, metadata=None):
metadata=None,
):
return grpc.experimental.unary_unary( return grpc.experimental.unary_unary(
request, request,
target, target,
"/geyser.Geyser/GetVersion", '/geyser.Geyser/GetVersion',
geyser__pb2.GetVersionRequest.SerializeToString, geyser__pb2.GetVersionRequest.SerializeToString,
geyser__pb2.GetVersionResponse.FromString, geyser__pb2.GetVersionResponse.FromString,
options, options,
@@ -383,5 +481,4 @@ class Geyser:
wait_for_ready, wait_for_ready,
timeout, timeout,
metadata, metadata,
_registered_method=True, _registered_method=True)
)
File diff suppressed because one or more lines are too long
+45 -169
View File
@@ -2,13 +2,8 @@ from google.protobuf.internal import containers as _containers
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message from google.protobuf import message as _message
from typing import ( from collections.abc import Iterable as _Iterable, Mapping as _Mapping
ClassVar as _ClassVar, from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
Iterable as _Iterable,
Mapping as _Mapping,
Optional as _Optional,
Union as _Union,
)
DESCRIPTOR: _descriptor.FileDescriptor DESCRIPTOR: _descriptor.FileDescriptor
@@ -19,24 +14,18 @@ class RewardType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
Rent: _ClassVar[RewardType] Rent: _ClassVar[RewardType]
Staking: _ClassVar[RewardType] Staking: _ClassVar[RewardType]
Voting: _ClassVar[RewardType] Voting: _ClassVar[RewardType]
DeactivatedStake: _ClassVar[RewardType]
VATDebit: _ClassVar[RewardType]
Unspecified: RewardType Unspecified: RewardType
Fee: RewardType Fee: RewardType
Rent: RewardType Rent: RewardType
Staking: RewardType Staking: RewardType
Voting: RewardType Voting: RewardType
DeactivatedStake: RewardType
VATDebit: RewardType
class ConfirmedBlock(_message.Message): class ConfirmedBlock(_message.Message):
__slots__ = ( __slots__ = ("previous_blockhash", "blockhash", "parent_slot", "transactions", "rewards", "block_time", "block_height", "num_partitions")
"previous_blockhash",
"blockhash",
"parent_slot",
"transactions",
"rewards",
"block_time",
"block_height",
"num_partitions",
)
PREVIOUS_BLOCKHASH_FIELD_NUMBER: _ClassVar[int] PREVIOUS_BLOCKHASH_FIELD_NUMBER: _ClassVar[int]
BLOCKHASH_FIELD_NUMBER: _ClassVar[int] BLOCKHASH_FIELD_NUMBER: _ClassVar[int]
PARENT_SLOT_FIELD_NUMBER: _ClassVar[int] PARENT_SLOT_FIELD_NUMBER: _ClassVar[int]
@@ -53,19 +42,7 @@ class ConfirmedBlock(_message.Message):
block_time: UnixTimestamp block_time: UnixTimestamp
block_height: BlockHeight block_height: BlockHeight
num_partitions: NumPartitions num_partitions: NumPartitions
def __init__( def __init__(self, previous_blockhash: _Optional[str] = ..., blockhash: _Optional[str] = ..., parent_slot: _Optional[int] = ..., transactions: _Optional[_Iterable[_Union[ConfirmedTransaction, _Mapping]]] = ..., rewards: _Optional[_Iterable[_Union[Reward, _Mapping]]] = ..., block_time: _Optional[_Union[UnixTimestamp, _Mapping]] = ..., block_height: _Optional[_Union[BlockHeight, _Mapping]] = ..., num_partitions: _Optional[_Union[NumPartitions, _Mapping]] = ...) -> None: ...
self,
previous_blockhash: _Optional[str] = ...,
blockhash: _Optional[str] = ...,
parent_slot: _Optional[int] = ...,
transactions: _Optional[
_Iterable[_Union[ConfirmedTransaction, _Mapping]]
] = ...,
rewards: _Optional[_Iterable[_Union[Reward, _Mapping]]] = ...,
block_time: _Optional[_Union[UnixTimestamp, _Mapping]] = ...,
block_height: _Optional[_Union[BlockHeight, _Mapping]] = ...,
num_partitions: _Optional[_Union[NumPartitions, _Mapping]] = ...,
) -> None: ...
class ConfirmedTransaction(_message.Message): class ConfirmedTransaction(_message.Message):
__slots__ = ("transaction", "meta") __slots__ = ("transaction", "meta")
@@ -73,11 +50,7 @@ class ConfirmedTransaction(_message.Message):
META_FIELD_NUMBER: _ClassVar[int] META_FIELD_NUMBER: _ClassVar[int]
transaction: Transaction transaction: Transaction
meta: TransactionStatusMeta meta: TransactionStatusMeta
def __init__( def __init__(self, transaction: _Optional[_Union[Transaction, _Mapping]] = ..., meta: _Optional[_Union[TransactionStatusMeta, _Mapping]] = ...) -> None: ...
self,
transaction: _Optional[_Union[Transaction, _Mapping]] = ...,
meta: _Optional[_Union[TransactionStatusMeta, _Mapping]] = ...,
) -> None: ...
class Transaction(_message.Message): class Transaction(_message.Message):
__slots__ = ("signatures", "message") __slots__ = ("signatures", "message")
@@ -85,65 +58,47 @@ class Transaction(_message.Message):
MESSAGE_FIELD_NUMBER: _ClassVar[int] MESSAGE_FIELD_NUMBER: _ClassVar[int]
signatures: _containers.RepeatedScalarFieldContainer[bytes] signatures: _containers.RepeatedScalarFieldContainer[bytes]
message: Message message: Message
def __init__( def __init__(self, signatures: _Optional[_Iterable[bytes]] = ..., message: _Optional[_Union[Message, _Mapping]] = ...) -> None: ...
self,
signatures: _Optional[_Iterable[bytes]] = ...,
message: _Optional[_Union[Message, _Mapping]] = ...,
) -> None: ...
class Message(_message.Message): class Message(_message.Message):
__slots__ = ( __slots__ = ("header", "account_keys", "recent_blockhash", "instructions", "versioned", "address_table_lookups", "config")
"header",
"account_keys",
"recent_blockhash",
"instructions",
"versioned",
"address_table_lookups",
)
HEADER_FIELD_NUMBER: _ClassVar[int] HEADER_FIELD_NUMBER: _ClassVar[int]
ACCOUNT_KEYS_FIELD_NUMBER: _ClassVar[int] ACCOUNT_KEYS_FIELD_NUMBER: _ClassVar[int]
RECENT_BLOCKHASH_FIELD_NUMBER: _ClassVar[int] RECENT_BLOCKHASH_FIELD_NUMBER: _ClassVar[int]
INSTRUCTIONS_FIELD_NUMBER: _ClassVar[int] INSTRUCTIONS_FIELD_NUMBER: _ClassVar[int]
VERSIONED_FIELD_NUMBER: _ClassVar[int] VERSIONED_FIELD_NUMBER: _ClassVar[int]
ADDRESS_TABLE_LOOKUPS_FIELD_NUMBER: _ClassVar[int] ADDRESS_TABLE_LOOKUPS_FIELD_NUMBER: _ClassVar[int]
CONFIG_FIELD_NUMBER: _ClassVar[int]
header: MessageHeader header: MessageHeader
account_keys: _containers.RepeatedScalarFieldContainer[bytes] account_keys: _containers.RepeatedScalarFieldContainer[bytes]
recent_blockhash: bytes recent_blockhash: bytes
instructions: _containers.RepeatedCompositeFieldContainer[CompiledInstruction] instructions: _containers.RepeatedCompositeFieldContainer[CompiledInstruction]
versioned: bool versioned: bool
address_table_lookups: _containers.RepeatedCompositeFieldContainer[ address_table_lookups: _containers.RepeatedCompositeFieldContainer[MessageAddressTableLookup]
MessageAddressTableLookup config: TransactionConfig
] def __init__(self, header: _Optional[_Union[MessageHeader, _Mapping]] = ..., account_keys: _Optional[_Iterable[bytes]] = ..., recent_blockhash: _Optional[bytes] = ..., instructions: _Optional[_Iterable[_Union[CompiledInstruction, _Mapping]]] = ..., versioned: _Optional[bool] = ..., address_table_lookups: _Optional[_Iterable[_Union[MessageAddressTableLookup, _Mapping]]] = ..., config: _Optional[_Union[TransactionConfig, _Mapping]] = ...) -> None: ...
def __init__(
self, class TransactionConfig(_message.Message):
header: _Optional[_Union[MessageHeader, _Mapping]] = ..., __slots__ = ("priority_fee", "compute_unit_limit", "loaded_accounts_data_size_limit", "heap_size")
account_keys: _Optional[_Iterable[bytes]] = ..., PRIORITY_FEE_FIELD_NUMBER: _ClassVar[int]
recent_blockhash: _Optional[bytes] = ..., COMPUTE_UNIT_LIMIT_FIELD_NUMBER: _ClassVar[int]
instructions: _Optional[_Iterable[_Union[CompiledInstruction, _Mapping]]] = ..., LOADED_ACCOUNTS_DATA_SIZE_LIMIT_FIELD_NUMBER: _ClassVar[int]
versioned: bool = ..., HEAP_SIZE_FIELD_NUMBER: _ClassVar[int]
address_table_lookups: _Optional[ priority_fee: int
_Iterable[_Union[MessageAddressTableLookup, _Mapping]] compute_unit_limit: int
] = ..., loaded_accounts_data_size_limit: int
) -> None: ... heap_size: int
def __init__(self, priority_fee: _Optional[int] = ..., compute_unit_limit: _Optional[int] = ..., loaded_accounts_data_size_limit: _Optional[int] = ..., heap_size: _Optional[int] = ...) -> None: ...
class MessageHeader(_message.Message): class MessageHeader(_message.Message):
__slots__ = ( __slots__ = ("num_required_signatures", "num_readonly_signed_accounts", "num_readonly_unsigned_accounts")
"num_required_signatures",
"num_readonly_signed_accounts",
"num_readonly_unsigned_accounts",
)
NUM_REQUIRED_SIGNATURES_FIELD_NUMBER: _ClassVar[int] NUM_REQUIRED_SIGNATURES_FIELD_NUMBER: _ClassVar[int]
NUM_READONLY_SIGNED_ACCOUNTS_FIELD_NUMBER: _ClassVar[int] NUM_READONLY_SIGNED_ACCOUNTS_FIELD_NUMBER: _ClassVar[int]
NUM_READONLY_UNSIGNED_ACCOUNTS_FIELD_NUMBER: _ClassVar[int] NUM_READONLY_UNSIGNED_ACCOUNTS_FIELD_NUMBER: _ClassVar[int]
num_required_signatures: int num_required_signatures: int
num_readonly_signed_accounts: int num_readonly_signed_accounts: int
num_readonly_unsigned_accounts: int num_readonly_unsigned_accounts: int
def __init__( def __init__(self, num_required_signatures: _Optional[int] = ..., num_readonly_signed_accounts: _Optional[int] = ..., num_readonly_unsigned_accounts: _Optional[int] = ...) -> None: ...
self,
num_required_signatures: _Optional[int] = ...,
num_readonly_signed_accounts: _Optional[int] = ...,
num_readonly_unsigned_accounts: _Optional[int] = ...,
) -> None: ...
class MessageAddressTableLookup(_message.Message): class MessageAddressTableLookup(_message.Message):
__slots__ = ("account_key", "writable_indexes", "readonly_indexes") __slots__ = ("account_key", "writable_indexes", "readonly_indexes")
@@ -153,32 +108,10 @@ class MessageAddressTableLookup(_message.Message):
account_key: bytes account_key: bytes
writable_indexes: bytes writable_indexes: bytes
readonly_indexes: bytes readonly_indexes: bytes
def __init__( def __init__(self, account_key: _Optional[bytes] = ..., writable_indexes: _Optional[bytes] = ..., readonly_indexes: _Optional[bytes] = ...) -> None: ...
self,
account_key: _Optional[bytes] = ...,
writable_indexes: _Optional[bytes] = ...,
readonly_indexes: _Optional[bytes] = ...,
) -> None: ...
class TransactionStatusMeta(_message.Message): class TransactionStatusMeta(_message.Message):
__slots__ = ( __slots__ = ("err", "fee", "pre_balances", "post_balances", "inner_instructions", "inner_instructions_none", "log_messages", "log_messages_none", "pre_token_balances", "post_token_balances", "rewards", "loaded_writable_addresses", "loaded_readonly_addresses", "return_data", "return_data_none", "compute_units_consumed", "cost_units")
"err",
"fee",
"pre_balances",
"post_balances",
"inner_instructions",
"inner_instructions_none",
"log_messages",
"log_messages_none",
"pre_token_balances",
"post_token_balances",
"rewards",
"loaded_writable_addresses",
"loaded_readonly_addresses",
"return_data",
"return_data_none",
"compute_units_consumed",
)
ERR_FIELD_NUMBER: _ClassVar[int] ERR_FIELD_NUMBER: _ClassVar[int]
FEE_FIELD_NUMBER: _ClassVar[int] FEE_FIELD_NUMBER: _ClassVar[int]
PRE_BALANCES_FIELD_NUMBER: _ClassVar[int] PRE_BALANCES_FIELD_NUMBER: _ClassVar[int]
@@ -195,6 +128,7 @@ class TransactionStatusMeta(_message.Message):
RETURN_DATA_FIELD_NUMBER: _ClassVar[int] RETURN_DATA_FIELD_NUMBER: _ClassVar[int]
RETURN_DATA_NONE_FIELD_NUMBER: _ClassVar[int] RETURN_DATA_NONE_FIELD_NUMBER: _ClassVar[int]
COMPUTE_UNITS_CONSUMED_FIELD_NUMBER: _ClassVar[int] COMPUTE_UNITS_CONSUMED_FIELD_NUMBER: _ClassVar[int]
COST_UNITS_FIELD_NUMBER: _ClassVar[int]
err: TransactionError err: TransactionError
fee: int fee: int
pre_balances: _containers.RepeatedScalarFieldContainer[int] pre_balances: _containers.RepeatedScalarFieldContainer[int]
@@ -211,27 +145,8 @@ class TransactionStatusMeta(_message.Message):
return_data: ReturnData return_data: ReturnData
return_data_none: bool return_data_none: bool
compute_units_consumed: int compute_units_consumed: int
def __init__( cost_units: int
self, def __init__(self, err: _Optional[_Union[TransactionError, _Mapping]] = ..., fee: _Optional[int] = ..., pre_balances: _Optional[_Iterable[int]] = ..., post_balances: _Optional[_Iterable[int]] = ..., inner_instructions: _Optional[_Iterable[_Union[InnerInstructions, _Mapping]]] = ..., inner_instructions_none: _Optional[bool] = ..., log_messages: _Optional[_Iterable[str]] = ..., log_messages_none: _Optional[bool] = ..., pre_token_balances: _Optional[_Iterable[_Union[TokenBalance, _Mapping]]] = ..., post_token_balances: _Optional[_Iterable[_Union[TokenBalance, _Mapping]]] = ..., rewards: _Optional[_Iterable[_Union[Reward, _Mapping]]] = ..., loaded_writable_addresses: _Optional[_Iterable[bytes]] = ..., loaded_readonly_addresses: _Optional[_Iterable[bytes]] = ..., return_data: _Optional[_Union[ReturnData, _Mapping]] = ..., return_data_none: _Optional[bool] = ..., compute_units_consumed: _Optional[int] = ..., cost_units: _Optional[int] = ...) -> None: ...
err: _Optional[_Union[TransactionError, _Mapping]] = ...,
fee: _Optional[int] = ...,
pre_balances: _Optional[_Iterable[int]] = ...,
post_balances: _Optional[_Iterable[int]] = ...,
inner_instructions: _Optional[
_Iterable[_Union[InnerInstructions, _Mapping]]
] = ...,
inner_instructions_none: bool = ...,
log_messages: _Optional[_Iterable[str]] = ...,
log_messages_none: bool = ...,
pre_token_balances: _Optional[_Iterable[_Union[TokenBalance, _Mapping]]] = ...,
post_token_balances: _Optional[_Iterable[_Union[TokenBalance, _Mapping]]] = ...,
rewards: _Optional[_Iterable[_Union[Reward, _Mapping]]] = ...,
loaded_writable_addresses: _Optional[_Iterable[bytes]] = ...,
loaded_readonly_addresses: _Optional[_Iterable[bytes]] = ...,
return_data: _Optional[_Union[ReturnData, _Mapping]] = ...,
return_data_none: bool = ...,
compute_units_consumed: _Optional[int] = ...,
) -> None: ...
class TransactionError(_message.Message): class TransactionError(_message.Message):
__slots__ = ("err",) __slots__ = ("err",)
@@ -245,11 +160,7 @@ class InnerInstructions(_message.Message):
INSTRUCTIONS_FIELD_NUMBER: _ClassVar[int] INSTRUCTIONS_FIELD_NUMBER: _ClassVar[int]
index: int index: int
instructions: _containers.RepeatedCompositeFieldContainer[InnerInstruction] instructions: _containers.RepeatedCompositeFieldContainer[InnerInstruction]
def __init__( def __init__(self, index: _Optional[int] = ..., instructions: _Optional[_Iterable[_Union[InnerInstruction, _Mapping]]] = ...) -> None: ...
self,
index: _Optional[int] = ...,
instructions: _Optional[_Iterable[_Union[InnerInstruction, _Mapping]]] = ...,
) -> None: ...
class InnerInstruction(_message.Message): class InnerInstruction(_message.Message):
__slots__ = ("program_id_index", "accounts", "data", "stack_height") __slots__ = ("program_id_index", "accounts", "data", "stack_height")
@@ -261,13 +172,7 @@ class InnerInstruction(_message.Message):
accounts: bytes accounts: bytes
data: bytes data: bytes
stack_height: int stack_height: int
def __init__( def __init__(self, program_id_index: _Optional[int] = ..., accounts: _Optional[bytes] = ..., data: _Optional[bytes] = ..., stack_height: _Optional[int] = ...) -> None: ...
self,
program_id_index: _Optional[int] = ...,
accounts: _Optional[bytes] = ...,
data: _Optional[bytes] = ...,
stack_height: _Optional[int] = ...,
) -> None: ...
class CompiledInstruction(_message.Message): class CompiledInstruction(_message.Message):
__slots__ = ("program_id_index", "accounts", "data") __slots__ = ("program_id_index", "accounts", "data")
@@ -277,12 +182,7 @@ class CompiledInstruction(_message.Message):
program_id_index: int program_id_index: int
accounts: bytes accounts: bytes
data: bytes data: bytes
def __init__( def __init__(self, program_id_index: _Optional[int] = ..., accounts: _Optional[bytes] = ..., data: _Optional[bytes] = ...) -> None: ...
self,
program_id_index: _Optional[int] = ...,
accounts: _Optional[bytes] = ...,
data: _Optional[bytes] = ...,
) -> None: ...
class TokenBalance(_message.Message): class TokenBalance(_message.Message):
__slots__ = ("account_index", "mint", "ui_token_amount", "owner", "program_id") __slots__ = ("account_index", "mint", "ui_token_amount", "owner", "program_id")
@@ -296,14 +196,7 @@ class TokenBalance(_message.Message):
ui_token_amount: UiTokenAmount ui_token_amount: UiTokenAmount
owner: str owner: str
program_id: str program_id: str
def __init__( def __init__(self, account_index: _Optional[int] = ..., mint: _Optional[str] = ..., ui_token_amount: _Optional[_Union[UiTokenAmount, _Mapping]] = ..., owner: _Optional[str] = ..., program_id: _Optional[str] = ...) -> None: ...
self,
account_index: _Optional[int] = ...,
mint: _Optional[str] = ...,
ui_token_amount: _Optional[_Union[UiTokenAmount, _Mapping]] = ...,
owner: _Optional[str] = ...,
program_id: _Optional[str] = ...,
) -> None: ...
class UiTokenAmount(_message.Message): class UiTokenAmount(_message.Message):
__slots__ = ("ui_amount", "decimals", "amount", "ui_amount_string") __slots__ = ("ui_amount", "decimals", "amount", "ui_amount_string")
@@ -315,13 +208,7 @@ class UiTokenAmount(_message.Message):
decimals: int decimals: int
amount: str amount: str
ui_amount_string: str ui_amount_string: str
def __init__( def __init__(self, ui_amount: _Optional[float] = ..., decimals: _Optional[int] = ..., amount: _Optional[str] = ..., ui_amount_string: _Optional[str] = ...) -> None: ...
self,
ui_amount: _Optional[float] = ...,
decimals: _Optional[int] = ...,
amount: _Optional[str] = ...,
ui_amount_string: _Optional[str] = ...,
) -> None: ...
class ReturnData(_message.Message): class ReturnData(_message.Message):
__slots__ = ("program_id", "data") __slots__ = ("program_id", "data")
@@ -329,30 +216,23 @@ class ReturnData(_message.Message):
DATA_FIELD_NUMBER: _ClassVar[int] DATA_FIELD_NUMBER: _ClassVar[int]
program_id: bytes program_id: bytes
data: bytes data: bytes
def __init__( def __init__(self, program_id: _Optional[bytes] = ..., data: _Optional[bytes] = ...) -> None: ...
self, program_id: _Optional[bytes] = ..., data: _Optional[bytes] = ...
) -> None: ...
class Reward(_message.Message): class Reward(_message.Message):
__slots__ = ("pubkey", "lamports", "post_balance", "reward_type", "commission") __slots__ = ("pubkey", "lamports", "post_balance", "reward_type", "commission", "commission_bps")
PUBKEY_FIELD_NUMBER: _ClassVar[int] PUBKEY_FIELD_NUMBER: _ClassVar[int]
LAMPORTS_FIELD_NUMBER: _ClassVar[int] LAMPORTS_FIELD_NUMBER: _ClassVar[int]
POST_BALANCE_FIELD_NUMBER: _ClassVar[int] POST_BALANCE_FIELD_NUMBER: _ClassVar[int]
REWARD_TYPE_FIELD_NUMBER: _ClassVar[int] REWARD_TYPE_FIELD_NUMBER: _ClassVar[int]
COMMISSION_FIELD_NUMBER: _ClassVar[int] COMMISSION_FIELD_NUMBER: _ClassVar[int]
COMMISSION_BPS_FIELD_NUMBER: _ClassVar[int]
pubkey: str pubkey: str
lamports: int lamports: int
post_balance: int post_balance: int
reward_type: RewardType reward_type: RewardType
commission: str commission: str
def __init__( commission_bps: str
self, def __init__(self, pubkey: _Optional[str] = ..., lamports: _Optional[int] = ..., post_balance: _Optional[int] = ..., reward_type: _Optional[_Union[RewardType, str]] = ..., commission: _Optional[str] = ..., commission_bps: _Optional[str] = ...) -> None: ...
pubkey: _Optional[str] = ...,
lamports: _Optional[int] = ...,
post_balance: _Optional[int] = ...,
reward_type: _Optional[_Union[RewardType, str]] = ...,
commission: _Optional[str] = ...,
) -> None: ...
class Rewards(_message.Message): class Rewards(_message.Message):
__slots__ = ("rewards", "num_partitions") __slots__ = ("rewards", "num_partitions")
@@ -360,11 +240,7 @@ class Rewards(_message.Message):
NUM_PARTITIONS_FIELD_NUMBER: _ClassVar[int] NUM_PARTITIONS_FIELD_NUMBER: _ClassVar[int]
rewards: _containers.RepeatedCompositeFieldContainer[Reward] rewards: _containers.RepeatedCompositeFieldContainer[Reward]
num_partitions: NumPartitions num_partitions: NumPartitions
def __init__( def __init__(self, rewards: _Optional[_Iterable[_Union[Reward, _Mapping]]] = ..., num_partitions: _Optional[_Union[NumPartitions, _Mapping]] = ...) -> None: ...
self,
rewards: _Optional[_Iterable[_Union[Reward, _Mapping]]] = ...,
num_partitions: _Optional[_Union[NumPartitions, _Mapping]] = ...,
) -> None: ...
class UnixTimestamp(_message.Message): class UnixTimestamp(_message.Message):
__slots__ = ("timestamp",) __slots__ = ("timestamp",)
@@ -1,28 +1,24 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services.""" """Client and server classes corresponding to protobuf-defined services."""
import grpc import grpc
import warnings import warnings
GRPC_GENERATED_VERSION = "1.71.0" GRPC_GENERATED_VERSION = '1.84.0'
GRPC_VERSION = grpc.__version__ GRPC_VERSION = grpc.__version__
_version_not_supported = False _version_not_supported = False
try: try:
from grpc._utilities import first_version_is_lower from grpc._utilities import first_version_is_lower
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
_version_not_supported = first_version_is_lower(
GRPC_VERSION, GRPC_GENERATED_VERSION
)
except ImportError: except ImportError:
_version_not_supported = True _version_not_supported = True
if _version_not_supported: if _version_not_supported:
raise RuntimeError( raise RuntimeError(
f"The grpc package installed is at version {GRPC_VERSION}," f'The grpc package installed is at version {GRPC_VERSION},'
+ f" but the generated code in solana_storage_pb2_grpc.py depends on" + ' but the generated code in solana_storage_pb2_grpc.py depends on'
+ f" grpcio>={GRPC_GENERATED_VERSION}." + f' grpcio>={GRPC_GENERATED_VERSION}.'
+ f" Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}" + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ f" or downgrade your generated code using grpcio-tools<={GRPC_VERSION}." + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
) )
+186 -6
View File
@@ -1,5 +1,6 @@
syntax = "proto3"; syntax = "proto3";
import "google/protobuf/timestamp.proto";
import public "solana-storage.proto"; import public "solana-storage.proto";
option go_package = "github.com/rpcpool/yellowstone-grpc/examples/golang/proto"; option go_package = "github.com/rpcpool/yellowstone-grpc/examples/golang/proto";
@@ -8,6 +9,9 @@ package geyser;
service Geyser { service Geyser {
rpc Subscribe(stream SubscribeRequest) returns (stream SubscribeUpdate) {} rpc Subscribe(stream SubscribeRequest) returns (stream SubscribeUpdate) {}
rpc SubscribeDeshred(stream SubscribeDeshredRequest) returns (stream SubscribeUpdateDeshred) {}
rpc SubscribeGossip(SubscribeGossipRequest) returns (stream SubscribeUpdateGossip) {}
rpc SubscribeReplayInfo(SubscribeReplayInfoRequest) returns (SubscribeReplayInfoResponse) {}
rpc Ping(PingRequest) returns (PongResponse) {} rpc Ping(PingRequest) returns (PongResponse) {}
rpc GetLatestBlockhash(GetLatestBlockhashRequest) returns (GetLatestBlockhashResponse) {} rpc GetLatestBlockhash(GetLatestBlockhashRequest) returns (GetLatestBlockhashResponse) {}
rpc GetBlockHeight(GetBlockHeightRequest) returns (GetBlockHeightResponse) {} rpc GetBlockHeight(GetBlockHeightRequest) returns (GetBlockHeightResponse) {}
@@ -20,10 +24,16 @@ enum CommitmentLevel {
PROCESSED = 0; PROCESSED = 0;
CONFIRMED = 1; CONFIRMED = 1;
FINALIZED = 2; FINALIZED = 2;
FIRST_SHRED_RECEIVED = 3; }
COMPLETED = 4;
CREATED_BANK = 5; enum SlotStatus {
DEAD = 6; SLOT_PROCESSED = 0;
SLOT_CONFIRMED = 1;
SLOT_FINALIZED = 2;
SLOT_FIRST_SHRED_RECEIVED = 3;
SLOT_COMPLETED = 4;
SLOT_CREATED_BANK = 5;
SLOT_DEAD = 6;
} }
message SubscribeRequest { message SubscribeRequest {
@@ -37,6 +47,25 @@ message SubscribeRequest {
optional CommitmentLevel commitment = 6; optional CommitmentLevel commitment = 6;
repeated SubscribeRequestAccountsDataSlice accounts_data_slice = 7; repeated SubscribeRequestAccountsDataSlice accounts_data_slice = 7;
optional SubscribeRequestPing ping = 9; optional SubscribeRequestPing ping = 9;
optional uint64 from_slot = 11;
map<string, SubscribeRequestFilterBlockFooter> block_footer = 12;
}
// Hash algorithm used to build the filter. Carried on the wire so future
// releases can add alternative algorithms (e.g., xxh3) without breaking
// existing clients. Readers should verify this matches a supported algorithm
// before deserializing.
enum CuckooHashAlgorithm {
SIP_HASH = 0;
}
message CuckooFilter {
bytes data = 1; // bucket data
uint32 bucket_count = 2; // number of buckets
uint32 entries_per_bucket = 3; // slots per bucket (typically 4)
uint32 fingerprint_bits = 4; // fingerprint size (8, 12, or 16)
uint64 hash_seed = 5; // seed for deterministic hashing
CuckooHashAlgorithm hash_algorithm = 6;
} }
message SubscribeRequestFilterAccounts { message SubscribeRequestFilterAccounts {
@@ -44,6 +73,8 @@ message SubscribeRequestFilterAccounts {
repeated string owner = 3; repeated string owner = 3;
repeated SubscribeRequestFilterAccountsFilter filters = 4; repeated SubscribeRequestFilterAccountsFilter filters = 4;
optional bool nonempty_txn_signature = 5; optional bool nonempty_txn_signature = 5;
optional CuckooFilter cuckoo_accounts_filter = 6;
} }
message SubscribeRequestFilterAccountsFilter { message SubscribeRequestFilterAccountsFilter {
@@ -75,6 +106,7 @@ message SubscribeRequestFilterAccountsFilterLamports {
message SubscribeRequestFilterSlots { message SubscribeRequestFilterSlots {
optional bool filter_by_commitment = 1; optional bool filter_by_commitment = 1;
optional bool interslot_updates = 2;
} }
message SubscribeRequestFilterTransactions { message SubscribeRequestFilterTransactions {
@@ -84,6 +116,19 @@ message SubscribeRequestFilterTransactions {
repeated string account_include = 3; repeated string account_include = 3;
repeated string account_exclude = 4; repeated string account_exclude = 4;
repeated string account_required = 6; repeated string account_required = 6;
optional CuckooFilter cuckoo_account_include = 7;
// ATA / token-account expansion control. When set, account_include /
// account_exclude / account_required also match against owners of
// pre/post token balances on each transaction. Absent = no expansion.
optional TokenAccountExpansionControlFlag token_accounts = 30;
}
enum TokenAccountExpansionControlFlag {
// Match an owner if it owns a pre OR post token balance on the tx.
ALL = 0;
// Match an owner whose token balance changed in amount (per
// `account_index`) or whose token account was closed.
BALANCE_CHANGED = 1;
} }
message SubscribeRequestFilterBlocks { message SubscribeRequestFilterBlocks {
@@ -91,12 +136,27 @@ message SubscribeRequestFilterBlocks {
optional bool include_transactions = 2; optional bool include_transactions = 2;
optional bool include_accounts = 3; optional bool include_accounts = 3;
optional bool include_entries = 4; optional bool include_entries = 4;
optional CuckooFilter cuckoo_account_include = 5;
} }
message SubscribeRequestFilterBlocksMeta {} message SubscribeRequestFilterBlocksMeta {}
message SubscribeRequestFilterEntry {} message SubscribeRequestFilterEntry {}
message SubscribeRequestFilterBlockFooter {}
// Filter for deshred transactions (transactions received before execution).
// Deshred transactions are received when entries are formed from shreds,
// BEFORE any execution occurs. No TransactionStatusMeta is available.
// Address lookup tables are resolved, so both static account keys and
// dynamically loaded addresses (from ALTs) are available for filtering.
message SubscribeRequestFilterDeshredTransactions {
optional bool vote = 1;
repeated string account_include = 2;
repeated string account_exclude = 3;
repeated string account_required = 4;
}
message SubscribeRequestAccountsDataSlice { message SubscribeRequestAccountsDataSlice {
uint64 offset = 1; uint64 offset = 1;
uint64 length = 2; uint64 length = 2;
@@ -106,6 +166,18 @@ message SubscribeRequestPing {
int32 id = 1; int32 id = 1;
} }
// Request message for the SubscribeDeshred RPC.
// Subscribes to deshred transactions (transactions received before execution).
// Deshred transactions are received when entries are formed from shreds,
// BEFORE any execution occurs. No TransactionStatusMeta is available.
// Address lookup tables are resolved, so both static and loaded addresses
// are available for filtering.
message SubscribeDeshredRequest {
map<string, SubscribeRequestFilterDeshredTransactions> deshred_transactions = 1;
optional SubscribeRequestPing ping = 2;
map<string, SubscribeRequestFilterSlots> slots = 3;
}
message SubscribeUpdate { message SubscribeUpdate {
repeated string filters = 1; repeated string filters = 1;
oneof update_oneof { oneof update_oneof {
@@ -118,13 +190,17 @@ message SubscribeUpdate {
SubscribeUpdatePong pong = 9; SubscribeUpdatePong pong = 9;
SubscribeUpdateBlockMeta block_meta = 7; SubscribeUpdateBlockMeta block_meta = 7;
SubscribeUpdateEntry entry = 8; SubscribeUpdateEntry entry = 8;
SubscribeUpdateBlockFooter block_footer = 12;
} }
google.protobuf.Timestamp created_at = 11;
} }
message SubscribeUpdateAccount { message SubscribeUpdateAccount {
SubscribeUpdateAccountInfo account = 1; SubscribeUpdateAccountInfo account = 1;
uint64 slot = 2; uint64 slot = 2;
bool is_startup = 3; bool is_startup = 3;
// if is_startup is true, bank id is None.
optional uint64 bank_id = 4;
} }
message SubscribeUpdateAccountInfo { message SubscribeUpdateAccountInfo {
@@ -141,13 +217,17 @@ message SubscribeUpdateAccountInfo {
message SubscribeUpdateSlot { message SubscribeUpdateSlot {
uint64 slot = 1; uint64 slot = 1;
optional uint64 parent = 2; optional uint64 parent = 2;
CommitmentLevel status = 3; SlotStatus status = 3;
optional string dead_error = 4; optional string dead_error = 4;
// FIRST_SHRED_RECEIVED and COMPLETED slot status does not belong to any bank.
optional uint64 bank_id = 5;
} }
message SubscribeUpdateTransaction { message SubscribeUpdateTransaction {
SubscribeUpdateTransactionInfo transaction = 1; SubscribeUpdateTransactionInfo transaction = 1;
uint64 slot = 2; uint64 slot = 2;
uint64 bank_id = 3;
} }
message SubscribeUpdateTransactionInfo { message SubscribeUpdateTransactionInfo {
@@ -164,6 +244,7 @@ message SubscribeUpdateTransactionStatus {
bool is_vote = 3; bool is_vote = 3;
uint64 index = 4; uint64 index = 4;
solana.storage.ConfirmedBlock.TransactionError err = 5; solana.storage.ConfirmedBlock.TransactionError err = 5;
uint64 bank_id = 6;
} }
message SubscribeUpdateBlock { message SubscribeUpdateBlock {
@@ -180,6 +261,7 @@ message SubscribeUpdateBlock {
repeated SubscribeUpdateAccountInfo accounts = 11; repeated SubscribeUpdateAccountInfo accounts = 11;
uint64 entries_count = 12; uint64 entries_count = 12;
repeated SubscribeUpdateEntry entries = 13; repeated SubscribeUpdateEntry entries = 13;
uint64 bank_id = 14;
} }
message SubscribeUpdateBlockMeta { message SubscribeUpdateBlockMeta {
@@ -192,6 +274,20 @@ message SubscribeUpdateBlockMeta {
string parent_blockhash = 7; string parent_blockhash = 7;
uint64 executed_transaction_count = 8; uint64 executed_transaction_count = 8;
uint64 entries_count = 9; uint64 entries_count = 9;
uint64 bank_id = 10;
}
// The Alpenglow block footer, sent as its own update as soon as the validator
// reports it. It does not wait for the block it belongs to. Consumers that need
// it alongside other per-block updates join on (slot, bank_id).
// The finalization, skip reward and notarization reward certificates are not
// exposed yet.
message SubscribeUpdateBlockFooter {
uint64 slot = 1;
uint64 bank_id = 2;
bytes bank_hash = 3;
uint64 block_producer_time_nanos = 4;
bytes block_user_agent = 5;
} }
message SubscribeUpdateEntry { message SubscribeUpdateEntry {
@@ -201,6 +297,23 @@ message SubscribeUpdateEntry {
bytes hash = 4; bytes hash = 4;
uint64 executed_transaction_count = 5; uint64 executed_transaction_count = 5;
uint64 starting_transaction_index = 6; // added in v1.18, for solana 1.17 value is always 0 uint64 starting_transaction_index = 6; // added in v1.18, for solana 1.17 value is always 0
uint64 bank_id = 7;
}
message SubscribeUpdateDeshredTransaction {
SubscribeUpdateDeshredTransactionInfo transaction = 1;
uint64 slot = 2;
}
message SubscribeUpdateDeshredTransactionInfo {
bytes signature = 1;
bool is_vote = 2;
solana.storage.ConfirmedBlock.Transaction transaction = 3;
repeated bytes loaded_writable_addresses = 4;
repeated bytes loaded_readonly_addresses = 5;
uint32 completed_data_set_starting_shred_index = 6;
uint32 completed_data_set_ending_shred_index_exclusive = 7;
} }
message SubscribeUpdatePing {} message SubscribeUpdatePing {}
@@ -209,8 +322,75 @@ message SubscribeUpdatePong {
int32 id = 1; int32 id = 1;
} }
message SubscribeUpdateDeshred {
repeated string filters = 1;
oneof update_oneof {
SubscribeUpdateDeshredTransaction deshred_transaction = 2;
SubscribeUpdatePing ping = 3;
SubscribeUpdatePong pong = 4;
// field 5 is reserved for created_at (below the oneof)
SubscribeUpdateSlot slot = 6;
}
google.protobuf.Timestamp created_at = 5;
}
message SubscribeGossipRequest {}
message SubscribeUpdateGossip {
oneof update_oneof {
SubscribeUpdateContactInfoNode node = 1;
SubscribeUpdateContactInfoRemoved removed = 2;
// Server keepalive; the stream can be silent for minutes on a stable cluster.
SubscribeUpdatePing ping = 3;
GossipTopology snapshot = 6;
}
google.protobuf.Timestamp created_at = 5;
// Table revision: for `snapshot` the revision copied, for `node`/`removed` the revision applied. Increases by one per change. Unset (0) for `ping`.
uint64 seq = 7;
}
// Full copy of the server-side gossip contact info table, sent when a client subscribes. Later `node`/`removed` updates carry `seq > snapshot.seq`.
message GossipTopology {
repeated SubscribeUpdateContactInfoNode nodes = 1;
}
message SubscribeUpdateContactInfoNode {
bytes pubkey = 1;
uint64 wallclock = 2;
uint64 outset = 3;
uint32 shred_version = 4;
uint32 version_major = 5;
uint32 version_minor = 6;
uint32 version_patch = 7;
uint32 version_commit = 8;
uint32 version_feature_set = 9;
uint32 version_client_id = 10;
optional string gossip = 11;
optional string tpu_quic = 12;
optional string tpu_forwards_quic = 13;
optional string tpu_vote_udp = 14;
optional string tpu_vote_quic = 15;
optional string tvu_udp = 16;
optional string tvu_quic = 17;
optional string serve_repair_udp = 18;
optional string serve_repair_quic = 19;
optional string rpc = 20;
optional string rpc_pubsub = 21;
optional string alpenglow = 22;
}
message SubscribeUpdateContactInfoRemoved {
bytes pubkey = 1;
}
// non-streaming methods // non-streaming methods
message SubscribeReplayInfoRequest {}
message SubscribeReplayInfoResponse {
optional uint64 first_available = 1;
}
message PingRequest { message PingRequest {
int32 count = 1; int32 count = 1;
} }
@@ -259,4 +439,4 @@ message IsBlockhashValidRequest {
message IsBlockhashValidResponse { message IsBlockhashValidResponse {
uint64 slot = 1; uint64 slot = 1;
bool valid = 2; bool valid = 2;
} }
+18
View File
@@ -32,6 +32,19 @@ message Message {
repeated CompiledInstruction instructions = 4; repeated CompiledInstruction instructions = 4;
bool versioned = 5; bool versioned = 5;
repeated MessageAddressTableLookup address_table_lookups = 6; repeated MessageAddressTableLookup address_table_lookups = 6;
// Set only for V1 transaction messages (SIMD-0385). Absent for legacy/V0.
optional TransactionConfig config = 7;
}
// Inline budget config introduced by the V1 transaction format (SIMD-0385),
// replacing ComputeBudget instructions. Each field is optional; unset fields
// fall back to the runtime defaults.
// https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0385-transaction-v1.md
message TransactionConfig {
optional uint64 priority_fee = 1;
optional uint32 compute_unit_limit = 2;
optional uint32 loaded_accounts_data_size_limit = 3;
optional uint32 heap_size = 4;
} }
message MessageHeader { message MessageHeader {
@@ -67,6 +80,8 @@ message TransactionStatusMeta {
// Available since Solana v1.10.35 / v1.11.6. // Available since Solana v1.10.35 / v1.11.6.
// Set to `None` for txs executed on earlier versions. // Set to `None` for txs executed on earlier versions.
optional uint64 compute_units_consumed = 16; optional uint64 compute_units_consumed = 16;
// Total transaction cost
optional uint64 cost_units = 17;
} }
message TransactionError { message TransactionError {
@@ -121,6 +136,8 @@ enum RewardType {
Rent = 2; Rent = 2;
Staking = 3; Staking = 3;
Voting = 4; Voting = 4;
DeactivatedStake = 5;
VATDebit = 6;
} }
message Reward { message Reward {
@@ -129,6 +146,7 @@ message Reward {
uint64 post_balance = 3; uint64 post_balance = 3;
RewardType reward_type = 4; RewardType reward_type = 4;
string commission = 5; string commission = 5;
string commission_bps = 6;
} }
message Rewards { message Rewards {
@@ -3,6 +3,7 @@ Universal Geyser listener that works with any platform through the interface sys
""" """
import asyncio import asyncio
import logging
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
import grpc import grpc
@@ -16,6 +17,64 @@ from utils.logger import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
def _describe_envelope(update: geyser_pb2.SubscribeUpdate) -> str:
"""Summarise the transaction format a coin was created in, for the log.
Transaction v1 (SIMD-0385) carries its compute budget inline on the message
as `config` instead of as ComputeBudget instructions, and geyser sets that
field only for v1. Its presence is the version test: the `versioned` flag is
true for v0 and v1 alike and cannot separate them.
Diagnostic only — nothing routes on this. Detection stays on
`meta.log_messages`, which reads the same for every transaction version.
Args:
update: The geyser update a TokenInfo was just parsed out of
Returns:
A one-line summary of the version, inline budget and reported cost
"""
transaction = update.transaction.transaction
message = transaction.transaction.message
if not message.HasField("config"):
return "v0" if message.versioned else "legacy"
config = message.config
# Every field has presence, and an unset one means zero rather than a
# runtime default, so report "unset" rather than implying a fallback.
parts = [
f"priority_fee={config.priority_fee} lamports"
if config.HasField("priority_fee")
else "priority_fee=unset",
f"cu_limit={config.compute_unit_limit}"
if config.HasField("compute_unit_limit")
else "cu_limit=unset",
f"data_size={config.loaded_accounts_data_size_limit}"
if config.HasField("loaded_accounts_data_size_limit")
else "data_size=unset",
]
if config.HasField("heap_size"):
parts.append(f"heap={config.heap_size}")
if transaction.meta.HasField("cost_units"):
parts.append(f"cost_units={transaction.meta.cost_units}")
return f"v1 ({', '.join(parts)})"
def _log_envelope(update: geyser_pb2.SubscribeUpdate) -> None:
"""Report the creating transaction's format, when debug logging is on.
The summary is built only if it will be printed: this sits between detection
and submission, which extreme_fast_mode keeps free of avoidable work.
Args:
update: The geyser update a TokenInfo was just parsed out of
"""
if logger.isEnabledFor(logging.DEBUG):
logger.debug(f"Creating transaction envelope: {_describe_envelope(update)}")
class UniversalGeyserListener(BaseTokenListener): class UniversalGeyserListener(BaseTokenListener):
"""Universal Geyser listener that works with any platform.""" """Universal Geyser listener that works with any platform."""
@@ -146,6 +205,7 @@ class UniversalGeyserListener(BaseTokenListener):
logger.info( logger.info(
f"New token detected: {token_info.name} ({token_info.symbol}) on {token_info.platform.value}" f"New token detected: {token_info.name} ({token_info.symbol}) on {token_info.platform.value}"
) )
_log_envelope(update)
# Apply filters # Apply filters
if match_string and not ( if match_string and not (
@@ -44,6 +44,7 @@ NO_INPUT = {
"pumpfun_listen_tokens_logsubscribe.py", "pumpfun_listen_tokens_logsubscribe.py",
"pumpfun_listen_tokens_blocksubscribe.py", "pumpfun_listen_tokens_blocksubscribe.py",
"pumpfun_listen_tokens_geyser.py", "pumpfun_listen_tokens_geyser.py",
"pumpfun_listen_tokens_deshred.py",
"pumpfun_listen_tokens_pumpportal.py", "pumpfun_listen_tokens_pumpportal.py",
"pumpfun_listen_migrations_logsubscribe.py", "pumpfun_listen_migrations_logsubscribe.py",
"pumpfun_listen_migrations_programsubscribe.py", "pumpfun_listen_migrations_programsubscribe.py",
+151 -1
View File
@@ -46,6 +46,23 @@ Offline machine checks, no network and no funds moved:
0.29; a failure here means the decode has regressed and the log route is 0.29; a failure here means the decode has regressed and the log route is
carrying the whole load again. carrying the whole load again.
The geyser route decodes protobuf rather than transaction bytes, so it goes
blind differently and is pinned on its own frame:
8. The committed geyser frame really is a v1 create_v2 — over geyser there is
no version number, so `Message.config` is the test. It exists only in the
stubs regenerated from yellowstone-grpc-proto 13.0.0-rc4; the previous
vendored proto stopped at field 6 and this check fails on a rollback.
9. The pump.fun parser reads that frame into a usable TokenInfo. A missing
proto field is skipped rather than raised on, so a stale proto degrades in
silence here — which is exactly why it is pinned.
10. With the logs stripped from the frame, the instruction decode still finds
the coin, and still leaves state_from_event False: `args.creator` is
user-supplied, so the curve must be read.
11. The inline v1 budget is readable as a known field. A v1 coin carries no
ComputeBudget instructions at all, so anything reading a budget from the
instruction list reads nothing for it.
Usage: Usage:
uv run tests/regression/verify_transaction_v1.py uv run tests/regression/verify_transaction_v1.py
""" """
@@ -60,6 +77,7 @@ PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT / "src")) sys.path.insert(0, str(PROJECT_ROOT / "src"))
from core.client import SolanaClient # noqa: E402 from core.client import SolanaClient # noqa: E402
from geyser.generated import geyser_pb2 # noqa: E402
from interfaces.core import Platform # noqa: E402 from interfaces.core import Platform # noqa: E402
from monitoring.universal_block_listener import UniversalBlockListener # noqa: E402 from monitoring.universal_block_listener import UniversalBlockListener # noqa: E402
from platforms import get_platform_implementations # noqa: E402 from platforms import get_platform_implementations # noqa: E402
@@ -81,6 +99,16 @@ V0_FIXTURE = (
/ "decode" / "decode"
/ "raw_create_v2_with_fee_bps_from_gettransaction.json" / "raw_create_v2_with_fee_bps_from_gettransaction.json"
) )
# One geyser SubscribeUpdate carrying a v1 create_v2, captured 2026-09-23. The
# geyser route has its own decode path and its own stubs, so it needs its own
# fixture: the getTransaction JSON above cannot exercise a protobuf frame.
GEYSER_V1_FIXTURE = (
PROJECT_ROOT
/ "cookbook"
/ "pumpfun"
/ "decode"
/ "raw_create_v2_v1_from_geyser.json"
)
# Solana tags a v1 transaction with this first byte (SIMD-0385 VersionByte). # Solana tags a v1 transaction with this first byte (SIMD-0385 VersionByte).
V1_VERSION_BYTE = 129 V1_VERSION_BYTE = 129
@@ -220,7 +248,9 @@ def check_log_route_detects_without_the_envelope() -> bool:
tx["transaction"] = [base64.b64encode(b"not a transaction").decode(), "base64"] tx["transaction"] = [base64.b64encode(b"not a transaction").decode(), "base64"]
token_info = listener._process_block_transactions([tx]) # noqa: SLF001 token_info = listener._process_block_transactions([tx]) # noqa: SLF001
if token_info is None: if token_info is None:
print(" a v1 create with unreadable bytes was missed — the logs are not the route") print(
" a v1 create with unreadable bytes was missed — the logs are not the route"
)
return False return False
return True return True
@@ -293,6 +323,113 @@ def check_examples_route_on_logs() -> bool:
return True return True
def _geyser_update() -> "geyser_pb2.SubscribeUpdate":
"""Rebuild the captured geyser frame from its committed bytes.
Returns:
The `SubscribeUpdate` exactly as it came off the wire
"""
update = geyser_pb2.SubscribeUpdate()
update.ParseFromString(
base64.b64decode(_load(GEYSER_V1_FIXTURE)["subscribe_update_base64"])
)
return update
def check_geyser_fixture_is_a_v1_create() -> bool:
"""The geyser fixture has to be a v1 create or the rest proves nothing.
Over geyser there is no version number to read: `Message.config` is set for
v1 and absent otherwise, and `versioned` is true for v0 and v1 alike. The
vendored proto carried no `config` field until the 13.0.0-rc4 regeneration,
so this check also fails if the stubs are ever rolled back.
"""
update = _geyser_update()
message = update.transaction.transaction.transaction.message
if not message.HasField("config"):
print(" fixture message has no config field — not a v1, or stale stubs")
return False
if update.transaction.transaction.meta.err.ByteSize():
print(" fixture create failed on chain; a failed create has no mint")
return False
logs = list(update.transaction.transaction.meta.log_messages)
if not any("Instruction: CreateV2" in line for line in logs):
print(" fixture carries no CreateV2 instruction")
return False
return True
def check_geyser_parser_reads_the_v1_create() -> bool:
"""The geyser route must reach a usable TokenInfo on a v1 coin.
An unknown field is skipped by protobuf rather than raised on, so a stale
proto degrades silently here — the frame still parses and only the budget
goes missing. That is precisely why this is pinned.
"""
parser = get_platform_implementations(
Platform.PUMP_FUN, _OfflineClient()
).event_parser
token_info = parser.parse_token_creation_from_geyser(_geyser_update())
if token_info is None:
print(" geyser 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_geyser_instruction_fallback_reads_v1() -> bool:
"""With the logs gone, the instruction decode still has to find the coin.
Same split as the block listener: logs are the route, instruction decoding
is the fallback, and each is pinned separately so neither can quietly start
carrying the other's load. The fallback must *not* set state_from_event —
`args.creator` is user-supplied and post-2026-04-28 may differ from the
canonical `BondingCurve.creator`, so the curve still has to be read.
"""
update = _geyser_update()
del update.transaction.transaction.meta.log_messages[:]
parser = get_platform_implementations(
Platform.PUMP_FUN, _OfflineClient()
).event_parser
token_info = parser.parse_token_creation_from_geyser(update)
if token_info is None:
print(" instruction fallback returned None for a v1 create_v2")
return False
if token_info.mint is None:
print(" instruction fallback produced no mint")
return False
if token_info.state_from_event:
print(" fallback set state_from_event off an instruction-parsed creator")
return False
return True
def check_geyser_exposes_the_v1_budget() -> bool:
"""The inline v1 budget must be readable as a known field, not guesswork.
Transaction v1 moved the compute budget off ComputeBudget instructions and
onto the message, so anything reading a coin's budget from the instruction
list now reads nothing at all for a v1 coin. The listener reports this at
debug; the point of the check is that the field survives regeneration.
"""
config = _geyser_update().transaction.transaction.transaction.message.config
if not config.HasField("compute_unit_limit"):
print(" v1 fixture carries no compute unit limit")
return False
if config.compute_unit_limit <= 0:
print(f" compute unit limit is {config.compute_unit_limit}")
return False
# Unset means zero in v1, never a runtime default, so a real coin that
# reaches execution has to have set this explicitly.
return True
def main() -> int: def main() -> int:
checks = [ checks = [
("every call site accepts v1", check_every_call_site_accepts_v1), ("every call site accepts v1", check_every_call_site_accepts_v1),
@@ -308,6 +445,19 @@ def main() -> int:
check_log_route_detects_without_the_envelope, check_log_route_detects_without_the_envelope,
), ),
("the envelope fallback reads v1 too", check_envelope_fallback_reads_v1), ("the envelope fallback reads v1 too", check_envelope_fallback_reads_v1),
(
"geyser fixture is a successful v1 create_v2",
check_geyser_fixture_is_a_v1_create,
),
(
"geyser parser reads the v1 create",
check_geyser_parser_reads_the_v1_create,
),
(
"geyser instruction fallback reads v1 without logs",
check_geyser_instruction_fallback_reads_v1,
),
("geyser exposes the inline v1 budget", check_geyser_exposes_the_v1_budget),
("examples route on logs too", check_examples_route_on_logs), ("examples route on logs too", check_examples_route_on_logs),
] ]
failed = 0 failed = 0
+312
View File
@@ -0,0 +1,312 @@
"""Measure what Geyser's pre-execution deshred stream buys you, and what it costs.
Read-only. No funds are moved and nothing is submitted.
`SubscribeDeshred` emits a transaction when entries are formed from shreds,
before execution. This races it against the ordinary executed `Subscribe`
stream on the same endpoint and reports two things:
1. Lead time, over every pump.fun transaction and over token creations
separately. Creates behave differently from general traffic and are the
number that matters for sniping.
2. How many creates the deshred stream cannot detect, and why. A coin created
through a router reaches the program as a CPI, and inner instructions are
produced *by* execution -- so they do not exist on a pre-execution stream
at all. Those coins arrive on deshred with the create invisible, and no
amount of decoding recovers them. This classifies every create as
top-level or inner to size that blind spot.
Measured 2026-09-23 over 300s: creates led by a 6.4ms median (92% of the time),
while 4 of 118 creates were CPI-shaped and therefore undetectable on deshred.
Usage:
uv run tools/compare_deshred_latency.py
uv run tools/compare_deshred_latency.py --duration 600
"""
import argparse
import collections
import os
import statistics
import struct
import sys
import threading
import time
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(PROJECT_ROOT / "src"))
import base58 # noqa: E402
import grpc # noqa: E402
from dotenv import load_dotenv # noqa: E402
from geyser.generated import geyser_pb2, geyser_pb2_grpc # noqa: E402
load_dotenv()
PUMP_PROGRAM_ID = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
# First 8 bytes of sha256("global:create") and sha256("global:createV2").
# Run cookbook/solana/anchor_calculate_discriminator.py to recompute them.
CREATE = struct.pack("<Q", 8576854823835016728)
CREATE_V2 = bytes([214, 144, 76, 236, 95, 139, 49, 180])
DEFAULT_DURATION = 300
def build_stub() -> geyser_pb2_grpc.GeyserStub:
"""Open an authenticated Geyser channel.
Returns:
A stub bound to the configured endpoint
"""
token = os.environ["GEYSER_API_TOKEN"]
endpoint = os.environ["GEYSER_ENDPOINT"]
auth = grpc.metadata_call_credentials(
lambda _, callback: callback((("x-token", token),), None)
)
creds = grpc.composite_channel_credentials(grpc.ssl_channel_credentials(), auth)
return geyser_pb2_grpc.GeyserStub(grpc.secure_channel(endpoint, creds))
def is_create(data: bytes) -> bool:
"""Report whether instruction data is a pump.fun create.
Args:
data: Raw instruction data
Returns:
True for `create` or `create_v2`
"""
return data.startswith(CREATE) or data.startswith(CREATE_V2)
def summarise(leads: list[float]) -> dict:
"""Reduce a list of lead times to the distribution worth printing.
Args:
leads: Lead times in milliseconds, positive when deshred was first
Returns:
Counts and percentiles
"""
ordered = sorted(leads)
count = len(ordered)
def at(fraction: float) -> float:
return ordered[min(count - 1, int(fraction * count))]
return {
"n": count,
"ahead": sum(1 for value in ordered if value > 0),
"p10": at(0.10),
"p25": at(0.25),
"median": statistics.median(ordered),
"p75": at(0.75),
"p90": at(0.90),
"p99": at(0.99),
"mean": statistics.mean(ordered),
"max": ordered[-1],
}
def report(label: str, leads: list[float]) -> None:
"""Print one lead-time distribution.
Args:
label: What this sample covers
leads: Lead times in milliseconds
"""
if not leads:
print(f"\n{label}: no overlapping signatures")
return
stats = summarise(leads)
share = 100 * stats["ahead"] / stats["n"]
print(f"\n{label} (n={stats['n']})")
print(f" deshred first : {stats['ahead']}/{stats['n']} ({share:.1f}%)")
print(
f" lead ms : p10 {stats['p10']:+.1f} | p25 {stats['p25']:+.1f} | "
f"median {stats['median']:+.1f} | p75 {stats['p75']:+.1f} | "
f"p90 {stats['p90']:+.1f} | p99 {stats['p99']:+.1f} | max {stats['max']:+.1f}"
)
print(f" mean : {stats['mean']:+.1f} ms")
class Collector:
"""Shared state both streams write into while the race runs."""
def __init__(self) -> None:
self.executed_at: dict[str, float] = {}
self.deshred_at: dict[str, float] = {}
# sig -> "top-level" or "inner (CPI)", from the executed stream, which is
# the only one that can tell the difference.
self.create_shape: dict[str, str] = {}
self.deshred_creates: set[str] = set()
self.deshred_all: set[str] = set()
self.errors: dict[str, str] = {}
def _keepalive(request, deadline: float): # noqa: ANN001, ANN202 - grpc generator
"""Hold the subscription open until the deadline passes."""
yield request
while time.time() < deadline:
time.sleep(0.5)
def collect_executed(collector: Collector, deadline: float) -> None:
"""Record arrival times and create shapes from the executed stream.
Args:
collector: Shared state to write into
deadline: Wall-clock time to stop at
"""
try:
request = geyser_pb2.SubscribeRequest()
request.transactions["pump"].account_include.append(PUMP_PROGRAM_ID)
request.commitment = geyser_pb2.CommitmentLevel.PROCESSED
for update in build_stub().Subscribe(_keepalive(request, deadline)):
seen = time.monotonic()
if time.time() > deadline:
break
if not update.HasField("transaction"):
continue
info = update.transaction.transaction
signature = base58.b58encode(bytes(info.signature)).decode()
collector.executed_at.setdefault(signature, seen)
if not any(
"Instruction: CreateV2" in line or line.endswith("Instruction: Create")
for line in info.meta.log_messages
):
continue
top_level = any(
is_create(ix.data) for ix in info.transaction.message.instructions
)
collector.create_shape[signature] = (
"top-level" if top_level else "inner (CPI)"
)
except grpc.RpcError as error:
collector.errors["executed"] = f"{error.code()}: {error.details()}"
def collect_deshred(collector: Collector, deadline: float) -> None:
"""Record arrival times and detectable creates from the deshred stream.
Args:
collector: Shared state to write into
deadline: Wall-clock time to stop at
"""
try:
request = geyser_pb2.SubscribeDeshredRequest()
filters = request.deshred_transactions["pump"]
filters.account_include.append(PUMP_PROGRAM_ID)
filters.vote = False
for update in build_stub().SubscribeDeshred(_keepalive(request, deadline)):
seen = time.monotonic()
if time.time() > deadline:
break
if not update.HasField("deshred_transaction"):
continue
info = update.deshred_transaction.transaction
signature = base58.b58encode(bytes(info.signature)).decode()
collector.deshred_at.setdefault(signature, seen)
collector.deshred_all.add(signature)
# No logs and no inner instructions here, by construction.
if any(is_create(ix.data) for ix in info.transaction.message.instructions):
collector.deshred_creates.add(signature)
except grpc.RpcError as error:
collector.errors["deshred"] = f"{error.code()}: {error.details()}"
def print_results(collector: Collector, duration: int) -> None:
"""Print the counts, the lead distributions and the blind-spot breakdown.
Args:
collector: State both streams wrote into
duration: Sampling window in seconds, for the header
"""
print(f"errors: {collector.errors or 'none'}")
print(f"window: {duration}s")
print(
f"executed stream : {len(collector.executed_at)} txs, "
f"{len(collector.create_shape)} creates"
)
print(
f"deshred stream : {len(collector.deshred_at)} txs, "
f"{len(collector.deshred_creates)} creates"
)
both = set(collector.executed_at) & set(collector.deshred_at)
print(
f"signatures in both: {len(both)} "
f"(deshred only {len(set(collector.deshred_at) - set(collector.executed_at))}, "
f"executed only {len(set(collector.executed_at) - set(collector.deshred_at))})"
)
leads = {
sig: (collector.executed_at[sig] - collector.deshred_at[sig]) * 1000
for sig in both
}
report("ALL pump.fun transactions", list(leads.values()))
report(
"CREATES only", [leads[sig] for sig in collector.create_shape if sig in leads]
)
shapes = collections.Counter(collector.create_shape.values())
print(f"\ncreate shapes seen: {dict(shapes)}")
missed = {
sig: shape
for sig, shape in collector.create_shape.items()
if sig not in collector.deshred_creates
}
print(
f"creates deshred could not detect: {len(missed)}/{len(collector.create_shape)}"
)
breakdown = collections.Counter(
f"{shape}, "
f"{'present on deshred' if sig in collector.deshred_all else 'ABSENT from deshred'}"
for sig, shape in missed.items()
)
for reason, count in breakdown.most_common():
print(f" {count:3d} {reason}")
print(
"\nA CPI create that is present but undetected is the structural limit: "
"inner instructions are produced by execution, so a pre-execution stream "
"never carries them."
)
def main() -> int:
"""Race the two streams and print the comparison.
Returns:
Process exit code
"""
parser = argparse.ArgumentParser(description="Compare deshred and executed streams")
parser.add_argument(
"--duration",
type=int,
default=DEFAULT_DURATION,
help=f"seconds to sample (default: {DEFAULT_DURATION})",
)
args = parser.parse_args()
collector = Collector()
deadline = time.time() + args.duration
threads = [
threading.Thread(target=collect_executed, args=(collector, deadline)),
threading.Thread(target=collect_deshred, args=(collector, deadline)),
]
for thread in threads:
thread.start()
for thread in threads:
thread.join(args.duration + 20)
print_results(collector, args.duration)
return 0
if __name__ == "__main__":
sys.exit(main())