mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-11-02 06:16:10 +00:00
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:
co-authored by
Claude Opus 5
parent
7c6d626e1c
commit
1e2de43292
@@ -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
@@ -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__":
|
||||||
|
|||||||
+167
-118
File diff suppressed because one or more lines are too long
+348
-385
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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}.'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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())
|
||||||
Reference in New Issue
Block a user