Skip to content

Commit 37c9d22

Browse files
author
Anton Sauchyk
committed
feat: add ping
1 parent cf8c4c0 commit 37c9d22

File tree

3 files changed

+128
-38
lines changed

3 files changed

+128
-38
lines changed

main.py

Lines changed: 90 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
1717
PUMP_CREATE_PREFIX = struct.pack("<Q", 8576854823835016728)
1818

19+
1920
async def create_geyser_connection():
2021
"""Establish a secure connection to the Geyser endpoint."""
2122
if AUTH_TYPE == "x-token":
@@ -24,15 +25,19 @@ async def create_geyser_connection():
2425
)
2526
else: # Basic authentication
2627
auth = grpc.metadata_call_credentials(
27-
lambda _, callback: callback((("authorization", f"Basic {GEYSER_API_TOKEN}"),), None)
28+
lambda _, callback: callback(
29+
(("authorization", f"Basic {GEYSER_API_TOKEN}"),), None
30+
)
2831
)
29-
32+
3033
creds = grpc.composite_channel_credentials(grpc.ssl_channel_credentials(), auth)
34+
35+
# gRPC keepalive options (complementary to Yellowstone pings)
3136
keepalive_options = [
32-
('grpc.keepalive_time_ms', 30000),
33-
('grpc.keepalive_timeout_ms', 10000),
34-
('grpc.keepalive_permit_without_calls', True),
35-
('grpc.http2.min_time_between_pings_ms', 10000),
37+
("grpc.keepalive_time_ms", 30000),
38+
("grpc.keepalive_timeout_ms", 10000),
39+
("grpc.keepalive_permit_without_calls", True),
40+
("grpc.http2.min_time_between_pings_ms", 10000),
3641
]
3742

3843
channel = grpc.aio.secure_channel(GEYSER_ENDPOINT, creds, options=keepalive_options)
@@ -48,48 +53,66 @@ def create_subscription_request():
4853
return request
4954

5055

56+
def create_ping_request():
57+
"""Create a ping request to keep connection alive."""
58+
ping_request = geyser_pb2.SubscribeRequest()
59+
ping_request.ping = True # Yellowstone-specific ping
60+
return ping_request
61+
62+
63+
async def request_generator():
64+
"""Generate subscription requests with periodic pings."""
65+
# Send initial subscription
66+
yield create_subscription_request()
67+
68+
# Send pings every 30 seconds to keep connection alive
69+
while True:
70+
await asyncio.sleep(30)
71+
yield create_ping_request()
72+
73+
5174
def decode_create_instruction(ix_data: bytes, keys, accounts) -> dict:
5275
"""Decode a create instruction from transaction data."""
5376
offset = 8 # Skip the 8-byte discriminator
54-
77+
5578
def get_account_key(index):
5679
"""Extract account public key by index."""
5780
if index >= len(accounts):
5881
return "N/A"
5982
account_index = accounts[index]
6083
return base58.b58encode(keys[account_index]).decode()
61-
84+
6285
def read_string():
6386
"""Read length-prefixed string from instruction data."""
6487
nonlocal offset
6588
length = struct.unpack_from("<I", ix_data, offset)[0] # Read 4-byte length
6689
offset += 4
67-
value = ix_data[offset:offset + length].decode() # Read string data
90+
value = ix_data[offset : offset + length].decode() # Read string data
6891
offset += length
6992
return value
70-
93+
7194
def read_pubkey():
7295
"""Read 32-byte public key from instruction data."""
7396
nonlocal offset
74-
value = base58.b58encode(ix_data[offset:offset + 32]).decode()
97+
value = base58.b58encode(ix_data[offset : offset + 32]).decode()
7598
offset += 32
7699
return value
77-
100+
78101
# Parse instruction data according to pump.fun's create schema
79102
name = read_string()
80-
symbol = read_string()
103+
symbol = read_string()
81104
uri = read_string()
82105
creator = read_pubkey()
83-
106+
84107
return {
85108
"name": name,
86109
"symbol": symbol,
87110
"uri": uri,
88111
"creator": creator,
89-
"mint": get_account_key(0), # New token mint address
112+
"mint": get_account_key(0), # New token mint address
90113
"bonding_curve": get_account_key(2), # Price discovery mechanism
91114
"associated_bonding_curve": get_account_key(3), # Token account for curve
92-
"user": get_account_key(7), # Transaction signer
115+
"user": get_account_key(7), # Transaction signer
93116
}
94117

95118

@@ -107,30 +130,59 @@ def print_token_info(info, signature):
107130
async def monitor_pump():
108131
"""Monitor Solana blockchain for new Pump.fun token creations."""
109132
print(f"Starting Pump.fun token monitor using {AUTH_TYPE.upper()} authentication")
110-
stub = await create_geyser_connection()
111-
request = create_subscription_request()
112-
113-
async for update in stub.Subscribe(iter([request])):
114-
# Only process transaction updates
115-
if not update.HasField("transaction"):
116-
continue
117-
118-
tx = update.transaction.transaction.transaction
119-
msg = getattr(tx, "message", None)
120-
if msg is None:
121-
continue
122-
123-
# Check each instruction in the transaction
124-
for ix in msg.instructions:
125-
# Quick check: is this a pump.fun create instruction?
126-
if not ix.data.startswith(PUMP_CREATE_PREFIX):
133+
print("📡 Connecting to Yellowstone gRPC...")
134+
135+
try:
136+
stub = await create_geyser_connection()
137+
print("✅ Connected successfully!")
138+
print("🔍 Monitoring for new Pump.fun token launches...")
139+
print("🏓 Ping system active (every 30s)")
140+
print("-" * 50)
141+
142+
async for update in stub.Subscribe(request_generator()):
143+
# Handle ping/pong responses
144+
if update.HasField("pong"):
145+
print("🏓 Pong received from server")
146+
continue
147+
148+
# Only process transaction updates
149+
if not update.HasField("transaction"):
150+
continue
151+
152+
tx = update.transaction.transaction.transaction
153+
msg = getattr(tx, "message", None)
154+
if msg is None:
127155
continue
128156

129-
# Decode and display token information
130-
info = decode_create_instruction(ix.data, msg.account_keys, ix.accounts)
131-
signature = base58.b58encode(bytes(update.transaction.transaction.signature)).decode()
132-
print_token_info(info, signature)
157+
# Check each instruction in the transaction
158+
for ix in msg.instructions:
159+
# Quick check: is this a pump.fun create instruction?
160+
if not ix.data.startswith(PUMP_CREATE_PREFIX):
161+
continue
162+
163+
# Decode and display token information
164+
try:
165+
info = decode_create_instruction(
166+
ix.data, msg.account_keys, ix.accounts
167+
)
168+
signature = base58.b58encode(
169+
bytes(update.transaction.transaction.signature)
170+
).decode()
171+
print_token_info(info, signature)
172+
except Exception as e:
173+
print(f"⚠️ Error decoding instruction: {e}")
174+
continue
175+
176+
except grpc.RpcError as e:
177+
print(f"❌ gRPC Error: {e}")
178+
except Exception as e:
179+
print(f"❌ Unexpected error: {e}")
133180

134181

135182
if __name__ == "__main__":
136-
asyncio.run(monitor_pump())
183+
try:
184+
asyncio.run(monitor_pump())
185+
except KeyboardInterrupt:
186+
print("\n🛑 Monitor stopped by user")
187+
except Exception as e:
188+
print(f"❌ Fatal error: {e}")

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,8 @@ dependencies = [
1111
"python-dotenv>=1.1.1",
1212
"solders>=0.26.0",
1313
]
14+
15+
[dependency-groups]
16+
dev = [
17+
"ruff>=0.12.3",
18+
]

uv.lock

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)