Merge pull request #94 from chainstacklabs/refactored/main-v2

Promoting refactored bot to the main branch
This commit is contained in:
Anton
2025-04-30 11:51:09 +00:00
committed by GitHub
94 changed files with 10936 additions and 1539 deletions
+5
View File
@@ -0,0 +1,5 @@
SOLANA_NODE_RPC_ENDPOINT=...
SOLANA_NODE_WSS_ENDPOINT=...
GEYSER_ENDPOINT=...
GEYSER_API_TOKEN=...
SOLANA_PRIVATE_KEY=...
+8 -1
View File
@@ -1,3 +1,9 @@
trades/*
.vscode
.pylintrc
.ruff_cache
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
@@ -159,4 +165,5 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
#.idea/
trades/trades.log
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+61 -2
View File
@@ -63,9 +63,68 @@ For near-instantaneous transaction propagation, you can use the [Chainstack Sola
[Sign up with Chainstack](https://console.chainstack.com).
Make sure you have the required packages installed `pip install -r requirements.txt`.
Make sure you have your endpoints set up in `config.py`.
## 🚀 Getting started
### Prerequisites
- Install [uv](https://github.com/astral-sh/uv), a fast Python package manager.
> If Python is already installed, `uv` will detect and use it automatically.
### Installation
#### 1️⃣ Install Python (if needed)
```bash
uv python install
```
> **Why?** `uv` will fetch and install the required Python version for your system.
#### 2️⃣ Clone the repository
```bash
git clone https://github.com/chainstacklabs/pump-fun-bot.git
cd pump-fun-bot
```
#### 3️⃣ Set up a virtual environment
```bash
# Create virtual environment
uv venv
# Activate (Unix/macOS)
source .venv/bin/activate
# Activate (Windows)
.venv\Scripts\activate
```
> Virtual environments help keep dependencies isolated and prevent conflicts.
#### 4️⃣ Install dependencies
```bash
uv pip install -e .
```
> **Why `-e` (editable mode)?** Lets you modify the code without reinstalling the package—useful for development!
#### 5️⃣ Configure the bot
```bash
# Copy example config
cp .env.example .env # Unix/macOS
# Windows
copy .env.example .env
```
Edit the `.env` file and add your **Solana RPC endpoints** and **private key**.
### Running the bot
```bash
# Option 1: run as installed package
pump_bot --help
# Option 2: run directly
python -m src.cli --help
```
> **You're all set! 🎉** Now you can start using the bot. Check `--help` for available commands. 🚀
## Note on limits
+71
View File
@@ -0,0 +1,71 @@
# This file defines comprehensive parameters and settings for the trading bot.
# Carefully review and adjust values to match your trading strategy and risk tolerance.
# Bot identification and connection settings
name: "bot-sniper-1"
env_file: ".env"
rpc_endpoint: "${SOLANA_NODE_RPC_ENDPOINT}"
wss_endpoint: "${SOLANA_NODE_WSS_ENDPOINT}"
private_key: "${SOLANA_PRIVATE_KEY}"
enabled: false # You can turn off the bot w/o removing its config
separate_process: true
# Geyser configuration (fastest method for getting updates)
geyser:
endpoint: "${GEYSER_ENDPOINT}"
api_token: "${GEYSER_API_TOKEN}"
# Trading parameters
# Control trade execution: amount of SOL per trade and acceptable price deviation
trade:
buy_amount: 0.0001 # Amount of SOL to spend when buying (in SOL)
buy_slippage: 0.3 # Maximum acceptable price deviation (0.3 = 30%)
sell_slippage: 0.3
# EXTREME FAST mode configuration
# When enabled, skips waiting for the bonding curve to stabilize and RPC price check.
# The bot buys the specified number of tokens directly, making the process faster but less precise.
extreme_fast_mode: true
extreme_fast_token_amount: 20 # Amount of tokens to buy
# Priority fee configuration
# Manage transaction speed and cost on the Solana network.
# Note: dynamic mode requires an additional RPC call, which slows down the buying process.
priority_fees:
enable_dynamic: false # Use latest transactions to estimate required fee (getRecentPrioritizationFees)
enable_fixed: true # Use fixed amount below
fixed_amount: 1_000_000 # Base fee in microlamports
extra_percentage: 0.0 # Percentage increase on riority fee regardless of the calculation method (0.1 = 10%)
hard_cap: 1_000_000 # Maximum allowable fee in microlamports to prevent excessive spending
# Filters for token selection
filters:
match_string: null # Only process tokens with this string in name/symbol
bro_address: null # Only trade tokens created by this user address
listener_type: "geyser" # Method for detecting new tokens: "logs", "blocks", or "geyser"
max_token_age: 0.001 # Maximum token age in seconds for processing
marry_mode: false # Only buy tokens, skip selling
yolo_mode: false # Continuously trade tokens
# Retry and timeout settings
retries:
max_attempts: 1 # Number of attempts for transaction submission
wait_after_creation: 15 # Seconds to wait after token creation (only if EXTREME FAST is disabled)
wait_after_buy: 15 # Holding period after buy transaction
wait_before_new_token: 15 # Pause between token trades
# Token and account management
cleanup:
# Cleanup mode determines when to manage token accounts. Options:
# "disabled": no cleanup will occur.
# "on_fail": only clean up if a buy transaction fails.
# "after_sell": clean up after selling.
# "post_session": clean up all empty accounts after a trading session ends.
mode: "post_session"
force_close_with_burn: false # Force burning remaining tokens before closing account
with_priority_fee: false # Use priority fees for cleanup transactions
# Node provider configuration (not implemented)
node:
max_rps: 25 # Maximum requests per second
+71
View File
@@ -0,0 +1,71 @@
# This file defines comprehensive parameters and settings for the trading bot.
# Carefully review and adjust values to match your trading strategy and risk tolerance.
# Bot identification and connection settings
name: "bot-sniper-2"
env_file: ".env"
rpc_endpoint: "${SOLANA_NODE_RPC_ENDPOINT}"
wss_endpoint: "${SOLANA_NODE_WSS_ENDPOINT}"
private_key: "${SOLANA_PRIVATE_KEY}"
enabled: true # You can turn off the bot w/o removing its config
separate_process: true
# Geyser configuration (fastest method for getting updates)
geyser:
endpoint: "${GEYSER_ENDPOINT}"
api_token: "${GEYSER_API_TOKEN}"
# Trading parameters
# Control trade execution: amount of SOL per trade and acceptable price deviation
trade:
buy_amount: 0.0001 # Amount of SOL to spend when buying (in SOL)
buy_slippage: 0.3 # Maximum acceptable price deviation (0.3 = 30%)
sell_slippage: 0.3
# EXTREME FAST mode configuration
# When enabled, skips waiting for the bonding curve to stabilize and RPC price check.
# The bot buys the specified number of tokens directly, making the process faster but less precise.
extreme_fast_mode: true
extreme_fast_token_amount: 20 # Amount of tokens to buy
# Priority fee configuration
# Manage transaction speed and cost on the Solana network.
# Note: dynamic mode requires an additional RPC call, which slows down the buying process.
priority_fees:
enable_dynamic: false # Use latest transactions to estimate required fee (getRecentPrioritizationFees)
enable_fixed: true # Use fixed amount below
fixed_amount: 200_000 # Base fee in microlamports
extra_percentage: 0.0 # Percentage increase on riority fee regardless of the calculation method (0.1 = 10%)
hard_cap: 200_000 # Maximum allowable fee in microlamports to prevent excessive spending
# Filters for token selection
filters:
match_string: null # Only process tokens with this string in name/symbol
bro_address: null # Only trade tokens created by this user address
listener_type: "logs" # Method for detecting new tokens: "logs", "blocks", or "geyser"
max_token_age: 0.001 # Maximum token age in seconds for processing
marry_mode: false # Only buy tokens, skip selling
yolo_mode: false # Continuously trade tokens
# Retry and timeout settings
retries:
max_attempts: 1 # Number of attempts for transaction submission
wait_after_creation: 15 # Seconds to wait after token creation (only if EXTREME FAST is disabled)
wait_after_buy: 15 # Holding period after buy transaction
wait_before_new_token: 15 # Pause between token trades
# Token and account management
cleanup:
# Cleanup mode determines when to manage token accounts. Options:
# "disabled": no cleanup will occur.
# "on_fail": only clean up if a buy transaction fails.
# "after_sell": clean up after selling.
# "post_session": clean up all empty accounts after a trading session ends.
mode: "post_session"
force_close_with_burn: false # Force burning remaining tokens before closing account
with_priority_fee: false # Use priority fees for cleanup transactions
# Node provider configuration (not implemented)
node:
max_rps: 25 # Maximum requests per second
-277
View File
@@ -1,277 +0,0 @@
import asyncio
import json
import base64
import struct
import base58
import hashlib
import websockets
import time
from solana.rpc.async_api import AsyncClient
from solana.transaction import Transaction
from solana.rpc.commitment import Confirmed
from solana.rpc.types import TxOpts
from solders.pubkey import Pubkey
from solders.keypair import Keypair
from solders.instruction import Instruction, AccountMeta
from solders.system_program import TransferParams, transfer
from solders.transaction import VersionedTransaction
from spl.token.instructions import get_associated_token_address
import spl.token.instructions as spl_token
from config import *
from construct import Struct, Int64ul, Flag
# Here and later all the discriminators are precalculated. See learning-examples/discriminator.py
EXPECTED_DISCRIMINATOR = struct.pack("<Q", 6966180631402821399)
TOKEN_DECIMALS = 6
class BondingCurveState:
_STRUCT = Struct(
"virtual_token_reserves" / Int64ul,
"virtual_sol_reserves" / Int64ul,
"real_token_reserves" / Int64ul,
"real_sol_reserves" / Int64ul,
"token_total_supply" / Int64ul,
"complete" / Flag
)
def __init__(self, data: bytes) -> None:
parsed = self._STRUCT.parse(data[8:])
self.__dict__.update(parsed)
async def get_pump_curve_state(conn: AsyncClient, curve_address: Pubkey) -> BondingCurveState:
response = await conn.get_account_info(curve_address)
if not response.value or not response.value.data:
raise ValueError("Invalid curve state: No data")
data = response.value.data
if data[:8] != EXPECTED_DISCRIMINATOR:
raise ValueError("Invalid curve state discriminator")
return BondingCurveState(data)
def calculate_pump_curve_price(curve_state: BondingCurveState) -> float:
if curve_state.virtual_token_reserves <= 0 or curve_state.virtual_sol_reserves <= 0:
raise ValueError("Invalid reserve state")
return (curve_state.virtual_sol_reserves / LAMPORTS_PER_SOL) / (curve_state.virtual_token_reserves / 10 ** TOKEN_DECIMALS)
async def buy_token(mint: Pubkey, bonding_curve: Pubkey, associated_bonding_curve: Pubkey, amount: float, slippage: float = 0.01, max_retries=5):
private_key = base58.b58decode(PRIVATE_KEY)
payer = Keypair.from_bytes(private_key)
async with AsyncClient(RPC_ENDPOINT) as client:
associated_token_account = get_associated_token_address(payer.pubkey(), mint)
amount_lamports = int(amount * LAMPORTS_PER_SOL)
# Fetch the token price
curve_state = await get_pump_curve_state(client, bonding_curve)
token_price_sol = calculate_pump_curve_price(curve_state)
token_amount = amount / token_price_sol
# Calculate maximum SOL to spend with slippage
max_amount_lamports = int(amount_lamports * (1 + slippage))
# Create associated token account with retries
for ata_attempt in range(max_retries):
try:
account_info = await client.get_account_info(associated_token_account)
if account_info.value is None:
print(f"Creating associated token account (Attempt {ata_attempt + 1})...")
create_ata_ix = spl_token.create_associated_token_account(
payer=payer.pubkey(),
owner=payer.pubkey(),
mint=mint
)
create_ata_tx = Transaction()
create_ata_tx.add(create_ata_ix)
recent_blockhash = await client.get_latest_blockhash()
create_ata_tx.recent_blockhash = recent_blockhash.value.blockhash
await client.send_transaction(create_ata_tx, payer)
print("Associated token account created.")
print(f"Associated token account address: {associated_token_account}")
break
else:
print("Associated token account already exists.")
print(f"Associated token account address: {associated_token_account}")
break
except Exception as e:
print(f"Attempt {ata_attempt + 1} to create associated token account failed: {str(e)}")
if ata_attempt < max_retries - 1:
wait_time = 2 ** ata_attempt
print(f"Retrying in {wait_time} seconds...")
await asyncio.sleep(wait_time)
else:
print("Max retries reached. Unable to create associated token account.")
return
# Continue with the buy transaction
for attempt in range(max_retries):
try:
accounts = [
AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False),
AccountMeta(pubkey=PUMP_FEE, is_signer=False, is_writable=True),
AccountMeta(pubkey=mint, is_signer=False, is_writable=False),
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
AccountMeta(pubkey=associated_bonding_curve, is_signer=False, is_writable=True),
AccountMeta(pubkey=associated_token_account, is_signer=False, is_writable=True),
AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True),
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(pubkey=SYSTEM_RENT, is_signer=False, is_writable=False),
AccountMeta(pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False),
AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False),
]
discriminator = struct.pack("<Q", 16927863322537952870)
data = discriminator + struct.pack("<Q", int(token_amount * 10**6)) + struct.pack("<Q", max_amount_lamports)
buy_ix = Instruction(PUMP_PROGRAM, data, accounts)
recent_blockhash = await client.get_latest_blockhash()
transaction = Transaction()
transaction.add(buy_ix)
transaction.recent_blockhash = recent_blockhash.value.blockhash
tx = await client.send_transaction(
transaction,
payer,
opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed),
)
print(f"Transaction sent: https://explorer.solana.com/tx/{tx.value}")
await client.confirm_transaction(tx.value, commitment="confirmed")
print("Transaction confirmed")
return tx.value
except Exception as e:
print(f"Attempt {attempt + 1} failed: {str(e)}")
if attempt < max_retries - 1:
wait_time = 2 ** attempt
print(f"Retrying in {wait_time} seconds...")
await asyncio.sleep(wait_time)
else:
print("Max retries reached. Unable to complete the transaction.")
def load_idl(file_path):
with open(file_path, 'r') as f:
return json.load(f)
def decode_create_instruction(ix_data, ix_def, accounts):
args = {}
offset = 8 # Skip 8-byte discriminator
for arg in ix_def['args']:
if arg['type'] == 'string':
length = struct.unpack_from('<I', ix_data, offset)[0]
offset += 4
value = ix_data[offset:offset+length].decode('utf-8')
offset += length
elif arg['type'] == 'publicKey':
value = base64.b64encode(ix_data[offset:offset+32]).decode('utf-8')
offset += 32
else:
raise ValueError(f"Unsupported type: {arg['type']}")
args[arg['name']] = value
# Add accounts
args['mint'] = str(accounts[0])
args['bondingCurve'] = str(accounts[2])
args['associatedBondingCurve'] = str(accounts[3])
args['user'] = str(accounts[7])
return args
async def listen_for_create_transaction(websocket):
idl = load_idl('idl/pump_fun_idl.json')
create_discriminator = 8576854823835016728
subscription_message = json.dumps({
"jsonrpc": "2.0",
"id": 1,
"method": "blockSubscribe",
"params": [
{"mentionsAccountOrProgram": str(PUMP_PROGRAM)},
{
"commitment": "confirmed",
"encoding": "base64",
"showRewards": False,
"transactionDetails": "full",
"maxSupportedTransactionVersion": 0
}
]
})
await websocket.send(subscription_message)
print(f"Subscribed to blocks mentioning program: {PUMP_PROGRAM}")
ping_interval = 20
last_ping_time = time.time()
while True:
try:
current_time = time.time()
if current_time - last_ping_time > ping_interval:
await websocket.ping()
last_ping_time = current_time
response = await asyncio.wait_for(websocket.recv(), timeout=30)
data = json.loads(response)
if 'method' in data and data['method'] == 'blockNotification':
if 'params' in data and 'result' in data['params']:
block_data = data['params']['result']
if 'value' in block_data and 'block' in block_data['value']:
block = block_data['value']['block']
if 'transactions' in block:
for tx in block['transactions']:
if isinstance(tx, dict) and 'transaction' in tx:
tx_data_decoded = base64.b64decode(tx['transaction'][0])
transaction = VersionedTransaction.from_bytes(tx_data_decoded)
for ix in transaction.message.instructions:
if str(transaction.message.account_keys[ix.program_id_index]) == str(PUMP_PROGRAM):
ix_data = bytes(ix.data)
discriminator = struct.unpack('<Q', ix_data[:8])[0]
if discriminator == create_discriminator:
create_ix = next(instr for instr in idl['instructions'] if instr['name'] == 'create')
account_keys = [str(transaction.message.account_keys[index]) for index in ix.accounts]
decoded_args = decode_create_instruction(ix_data, create_ix, account_keys)
return decoded_args
except asyncio.TimeoutError:
print("No data received for 30 seconds, sending ping...")
await websocket.ping()
last_ping_time = time.time()
except websockets.exceptions.ConnectionClosed:
print("WebSocket connection closed. Reconnecting...")
raise
async def main(yolo_mode=False):
if yolo_mode:
while True:
try:
async with websockets.connect(WSS_ENDPOINT) as websocket:
while True:
try:
await trade(websocket)
except websockets.exceptions.ConnectionClosed:
print("WebSocket connection closed. Reconnecting...")
break
except Exception as e:
print(f"An error occurred: {e}")
print("Waiting for 5 seconds before looking for the next token...")
await asyncio.sleep(5)
except Exception as e:
print(f"Connection error: {e}")
print("Reconnecting in 5 seconds...")
await asyncio.sleep(5)
else:
await trade()
if __name__ == "__main__":
asyncio.run(main())
-27
View File
@@ -1,27 +0,0 @@
from solders.pubkey import Pubkey
# System & pump.fun addresses
PUMP_PROGRAM = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
PUMP_GLOBAL = Pubkey.from_string("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf")
PUMP_EVENT_AUTHORITY = Pubkey.from_string("Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1")
PUMP_FEE = Pubkey.from_string("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM")
PUMP_LIQUIDITY_MIGRATOR = Pubkey.from_string("39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg")
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL")
SYSTEM_RENT = Pubkey.from_string("SysvarRent111111111111111111111111111111111")
SOL = Pubkey.from_string("So11111111111111111111111111111111111111112")
LAMPORTS_PER_SOL = 1_000_000_000
# Trading parameters
BUY_AMOUNT = 0.0001 # Amount of SOL to spend when buying
BUY_SLIPPAGE = 0.2 # 20% slippage tolerance for buying
SELL_SLIPPAGE = 0.2 # 20% slippage tolerance for selling
# Your nodes
# You can also get a trader node https://docs.chainstack.com/docs/solana-trader-nodes
RPC_ENDPOINT = "SOLANA_NODE_RPC_ENDPOINT"
WSS_ENDPOINT = "SOLANA_NODE_WSS_ENDPOINT"
#Private key
PRIVATE_KEY = "SOLANA_PRIVATE_KEY"
@@ -3,64 +3,81 @@ import hashlib
import json
import os
import sys
import websockets
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from config import WSS_ENDPOINT, PUMP_PROGRAM
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from core.pubkeys import PumpAddresses
WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
async def save_transaction(tx_data, tx_signature):
os.makedirs("blockSubscribe-transactions", exist_ok=True)
hashed_signature = hashlib.sha256(tx_signature.encode()).hexdigest()
file_path = os.path.join("blockSubscribe-transactions", f"{hashed_signature}.json")
with open(file_path, 'w') as f:
with open(file_path, "w") as f:
json.dump(tx_data, f, indent=2)
print(f"Saved transaction: {hashed_signature[:8]}...")
async def listen_for_transactions():
async with websockets.connect(WSS_ENDPOINT) as websocket:
subscription_message = json.dumps({
"jsonrpc": "2.0",
"id": 1,
"method": "blockSubscribe",
"params": [
{"mentionsAccountOrProgram": str(PUMP_PROGRAM)},
{
"commitment": "confirmed",
"encoding": "base64",
"showRewards": False,
"transactionDetails": "full",
"maxSupportedTransactionVersion": 0
}
]
})
subscription_message = json.dumps(
{
"jsonrpc": "2.0",
"id": 1,
"method": "blockSubscribe",
"params": [
{"mentionsAccountOrProgram": str(PumpAddresses.PROGRAM)},
{
"commitment": "confirmed",
"encoding": "base64",
"showRewards": False,
"transactionDetails": "full",
"maxSupportedTransactionVersion": 0,
},
],
},
)
await websocket.send(subscription_message)
print(f"Subscribed to blocks mentioning program: {PUMP_PROGRAM}")
print(f"Subscribed to blocks mentioning program: {PumpAddresses.PROGRAM}")
while True:
try:
response = await websocket.recv()
data = json.loads(response)
if 'method' in data and data['method'] == 'blockNotification':
if 'params' in data and 'result' in data['params']:
block_data = data['params']['result']
if 'value' in block_data and 'block' in block_data['value']:
block = block_data['value']['block']
if 'transactions' in block:
transactions = block['transactions']
if "method" in data and data["method"] == "blockNotification":
if "params" in data and "result" in data["params"]:
block_data = data["params"]["result"]
if "value" in block_data and "block" in block_data["value"]:
block = block_data["value"]["block"]
if "transactions" in block:
transactions = block["transactions"]
for tx in transactions:
if isinstance(tx, dict) and 'transaction' in tx:
if isinstance(tx['transaction'], list) and len(tx['transaction']) > 0:
tx_signature = tx['transaction'][0]
elif isinstance(tx['transaction'], dict) and 'signatures' in tx['transaction']:
tx_signature = tx['transaction']['signatures'][0]
if isinstance(tx, dict) and "transaction" in tx:
if (
isinstance(tx["transaction"], list)
and len(tx["transaction"]) > 0
):
tx_signature = tx["transaction"][0]
elif (
isinstance(tx["transaction"], dict)
and "signatures" in tx["transaction"]
):
tx_signature = tx["transaction"][
"signatures"
][0]
else:
continue
await save_transaction(tx, tx_signature)
elif 'result' in data:
print(f"Subscription confirmed")
elif "result" in data:
print("Subscription confirmed")
except Exception as e:
print(f"An error occurred: {str(e)}")
print(f"An error occurred: {e!s}")
if __name__ == "__main__":
asyncio.run(listen_for_transactions())
asyncio.run(listen_for_transactions())
@@ -0,0 +1,156 @@
"""
Module for checking the status of a token's bonding curve on the Solana network using
the Pump.fun program. It allows querying the bonding curve state and completion status.
"""
import asyncio
import os
import struct
from typing import Final
from construct import Flag, Int64ul, Struct
from dotenv import load_dotenv
from solana.rpc.async_api import AsyncClient
from solders.pubkey import Pubkey
load_dotenv()
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
# Change to token you want to query
TOKEN_MINT = "xWrzYY4c1LnbSkLrd2LDUg9vw7YtVyJhGmw7MABpump"
# Constants
PUMP_PROGRAM_ID: Final[Pubkey] = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack("<Q", 6966180631402821399)
class BondingCurveState:
"""
Represents the state of a bonding curve account.
Attributes:
virtual_token_reserves: Virtual token reserves in the curve
virtual_sol_reserves: Virtual SOL reserves in the curve
real_token_reserves: Real token reserves in the curve
real_sol_reserves: Real SOL reserves in the curve
token_total_supply: Total token supply in the curve
complete: Whether the curve has completed and liquidity migrated
"""
_STRUCT = Struct(
"virtual_token_reserves" / Int64ul,
"virtual_sol_reserves" / Int64ul,
"real_token_reserves" / Int64ul,
"real_sol_reserves" / Int64ul,
"token_total_supply" / Int64ul,
"complete" / Flag,
)
def __init__(self, data: bytes) -> None:
parsed = self._STRUCT.parse(data[8:])
self.__dict__.update(parsed)
def get_associated_bonding_curve_address(
mint: Pubkey, program_id: Pubkey
) -> tuple[Pubkey, int]:
"""
Derives the associated bonding curve address for a given mint.
Args:
mint: The token mint address
program_id: The program ID for the bonding curve
Returns:
Tuple of (bonding curve address, bump seed)
"""
return Pubkey.find_program_address([b"bonding-curve", bytes(mint)], program_id)
async def get_bonding_curve_state(
conn: AsyncClient, curve_address: Pubkey
) -> BondingCurveState:
"""
Fetches and validates the state of a bonding curve account.
Args:
conn: AsyncClient connection to Solana RPC
curve_address: Address of the bonding curve account
Returns:
BondingCurveState object containing parsed account data
Raises:
ValueError: If account data is invalid or missing
"""
response = await conn.get_account_info(curve_address, encoding="base64")
if not response.value or not response.value.data:
raise ValueError("Invalid curve state: No data")
data = response.value.data
if data[:8] != EXPECTED_DISCRIMINATOR:
raise ValueError("Invalid curve state discriminator")
return BondingCurveState(data)
async def check_token_status(mint_address: str) -> None:
"""
Checks and prints the status of a token and its bonding curve.
Args:
mint_address: The token mint address as a string
"""
try:
mint = Pubkey.from_string(mint_address)
# Get the associated bonding curve address
bonding_curve_address, bump = get_associated_bonding_curve_address(
mint, PUMP_PROGRAM_ID
)
print("\nToken status:")
print("-" * 50)
print(f"Token mint: {mint}")
print(f"Associated bonding curve: {bonding_curve_address}")
print(f"Bump seed: {bump}")
print("-" * 50)
# Check completion status
async with AsyncClient(RPC_ENDPOINT) as client:
try:
curve_state = await get_bonding_curve_state(
client, bonding_curve_address
)
print("\nBonding curve status:")
print("-" * 50)
print(
f"Completion status: {'Completed' if curve_state.complete else 'Not completed'}"
)
if curve_state.complete:
print(
"\nNote: This bonding curve has completed and liquidity has been migrated to PumpSwap."
)
print("-" * 50)
except ValueError as e:
print(f"\nError accessing bonding curve: {e}")
except ValueError as e:
print(f"\nError: Invalid address format - {e}")
except Exception as e:
print(f"\nUnexpected error: {e}")
def main() -> None:
"""Main entry point for the token status checker."""
#parser = argparse.ArgumentParser(description="Check token bonding curve status")
#parser.add_argument("mint_address", help="The token mint address"
#args = parser.parse_args()
asyncio.run(check_token_status(TOKEN_MINT))
if __name__ == "__main__":
main()
@@ -0,0 +1,162 @@
"""
Module for querying and analyzing soon-to-gradute tokens in the Pump.fun program.
It includes functionality to fetch bonding curves based on token reserves and
find associated SPL token accounts.
Note: getProgramAccounts may be slow as it is a pretty heavy method for RPC.
"""
import asyncio
import os
import struct
from typing import Final
from dotenv import load_dotenv
from solana.rpc.async_api import AsyncClient
from solana.rpc.types import MemcmpOpts, TokenAccountOpts
from solders.pubkey import Pubkey
load_dotenv()
# Constants
RPC_ENDPOINT: Final[str] = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
PUMP_PROGRAM_ID: Final[Pubkey] = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
TOKEN_PROGRAM_ID: Final[Pubkey] = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
# The 8-byte discriminator for bonding curve accounts in Pump.fun
BONDING_CURVE_DISCRIMINATOR_BYTES: Final[bytes] = bytes.fromhex("17b7f83760d8ac60")
async def get_bonding_curves_by_reserves(client: AsyncClient | None = None) -> list:
"""
Fetch bonding curve accounts with real token reserves below a threshold.
Args:
client: Optional AsyncClient instance. If None, a new one will be created.
Returns:
List of bonding curve accounts matching the criteria
"""
# Define the reserve threshold (100 trillion in token base units)
threshold: int = 100_000_000_000_000
threshold_bytes: bytes = threshold.to_bytes(8, "little")
msb_prefix: bytes = threshold_bytes[6:] # Most significant bytes for pre-filtering
should_close_client: bool = client is None
try:
if should_close_client:
client = AsyncClient(RPC_ENDPOINT, commitment="processed", timeout=180)
await client.is_connected()
# Define on-chain filters for getProgramAccounts
filters = [
MemcmpOpts(offset=0, bytes=BONDING_CURVE_DISCRIMINATOR_BYTES), # Match bonding curve accounts
MemcmpOpts(offset=30, bytes=msb_prefix), # Pre-filter by real token reserves MSB
MemcmpOpts(offset=48, bytes=b"\x00"), # Ensure complete flag is False
]
# Query accounts matching filters
response = await client.get_program_accounts(
PUMP_PROGRAM_ID,
encoding="base64",
filters=filters
)
result = []
for acc in response.value:
raw = acc.account.data
# Extract real_token_reserves (u64 = 8 bytes, little-endian)
offset: int = 24 # real_token_reserves field offset
real_token_reserves: int = struct.unpack("<Q", raw[offset:offset + 8])[0]
# Post-filter: ensure value is below the threshold
if real_token_reserves < threshold:
print(f"Pubkey: {acc.pubkey}")
print(f"Real token reserves: {real_token_reserves / 10**6} tokens")
print("=" * 50)
result.append(acc)
return result
finally:
if should_close_client and client:
await client.close()
async def find_associated_bonding_curve(
bonding_curve_address: str, client: AsyncClient | None = None
) -> dict | None:
"""
Find the SPL token account owned by a bonding curve.
Args:
bonding_curve_address: The bonding curve public key (as a string)
client: Optional AsyncClient instance. If None, a new one will be created.
Returns:
The associated SPL token account data or None if not found
"""
should_close_client: bool = client is None
try:
if should_close_client:
client = AsyncClient(RPC_ENDPOINT)
await client.is_connected()
response = await client.get_token_accounts_by_owner(
Pubkey.from_string(bonding_curve_address),
TokenAccountOpts(program_id=TOKEN_PROGRAM_ID)
)
if response.value and len(response.value) > 0:
return response.value[0].account
else:
print(f"No token accounts found for {bonding_curve_address}")
return None
except Exception as e:
print(f"Error finding associated token account: {e}")
return None
finally:
if should_close_client and client:
await client.close()
def get_mint_address(data: bytes) -> str:
"""
Extract the mint address from SPL token account data.
Args:
data: The token account data as bytes
Returns:
The mint address as a base58-encoded string
"""
return str(Pubkey(data[:32]))
async def main() -> None:
"""Main entry point for querying and processing bonding curves."""
async with AsyncClient(RPC_ENDPOINT, commitment="processed", timeout=120) as client:
await client.is_connected()
bonding_curves = await get_bonding_curves_by_reserves(client)
print(f"Total matches: {len(bonding_curves)}")
print("=" * 50)
for bonding_curve in bonding_curves:
# Find the SPL token account owned by the bonding curve
associated_token_account = await find_associated_bonding_curve(
str(bonding_curve.pubkey), client
)
if associated_token_account:
mint_address = get_mint_address(associated_token_account.data)
print(f"Bonding curve: {bonding_curve.pubkey}")
print(f"Mint address: {mint_address}")
print("=" * 50)
# For demonstration, only process the first curve
break
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,135 @@
"""
Module for tracking the progress of a bonding curve for a Pump.fun token.
It continuously polls the bonding curve state and prints updates at regular intervals.
"""
import asyncio
import os
import struct
from typing import Final
from dotenv import load_dotenv
from solana.rpc.async_api import AsyncClient
from solders.pubkey import Pubkey
load_dotenv()
# Constants
RPC_URL: Final[str] = os.getenv("SOLANA_NODE_RPC_ENDPOINT")
TOKEN_MINT: Final[str] = "xWrzYY4c1LnbSkLrd2LDUg9vw7YtVyJhGmw7MABpump"
PUMP_PROGRAM_ID: Final[Pubkey] = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
LAMPORTS_PER_SOL: Final[int] = 1_000_000_000
TOKEN_DECIMALS: Final[int] = 6
EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack("<Q", 6966180631402821399) # Pump.fun bonding curve discriminator
POLL_INTERVAL: Final[int] = 10 # Seconds between each status check
def get_associated_bonding_curve_address(mint: Pubkey, program_id: Pubkey) -> Pubkey:
"""
Derive the bonding curve PDA address from a mint address.
Args:
mint: The token mint address
program_id: The program ID for the bonding curve
Returns:
The bonding curve address
"""
return Pubkey.find_program_address([b"bonding-curve", bytes(mint)], program_id)[0]
async def get_account_data(client: AsyncClient, pubkey: Pubkey) -> bytes:
"""
Fetch raw account data for a given public key.
Args:
client: AsyncClient connection to Solana RPC
pubkey: The public key of the account to fetch
Returns:
The raw account data as bytes
Raises:
ValueError: If the account is not found or has no data
"""
resp = await client.get_account_info(pubkey, encoding="base64")
if not resp.value or not resp.value.data:
raise ValueError(f"Account {pubkey} not found or has no data")
return resp.value.data
def parse_curve_state(data: bytes) -> dict:
"""
Decode bonding curve account data into a readable format.
Args:
data: The raw bonding curve account data
Returns:
A dictionary containing parsed bonding curve fields
Raises:
ValueError: If the account discriminator is invalid
"""
if data[:8] != EXPECTED_DISCRIMINATOR:
raise ValueError("Invalid discriminator for bonding curve")
fields = struct.unpack_from("<QQQQQ?", data, 8)
return {
"virtual_token_reserves": fields[0] / 10**TOKEN_DECIMALS,
"virtual_sol_reserves": fields[1] / LAMPORTS_PER_SOL,
"real_token_reserves": fields[2] / 10**TOKEN_DECIMALS,
"real_sol_reserves": fields[3] / LAMPORTS_PER_SOL,
"token_total_supply": fields[4] / 10**TOKEN_DECIMALS,
"complete": fields[5],
}
def print_curve_status(state: dict) -> None:
"""
Print the current status of the bonding curve in a readable format.
Args:
state: The parsed bonding curve state dictionary
"""
progress = 0
if state["token_total_supply"]:
progress = 100 - (100 * state["real_token_reserves"] / state["token_total_supply"])
print("=" * 30)
print(f"Complete: {'' if state['complete'] else ''}")
print(f"Progress: {progress:.2f}%")
print(f"Token reserves: {state['real_token_reserves']:.4f}")
print(f"SOL reserves: {state['real_sol_reserves']:.4f}")
print("=" * 30, "\n")
async def track_curve() -> None:
"""
Continuously track and display the state of a bonding curve.
"""
if not RPC_URL or not TOKEN_MINT:
print("❌ Set SOLANA_NODE_RPC_ENDPOINT and TOKEN_MINT in .env")
return
mint_pubkey: Pubkey = Pubkey.from_string(TOKEN_MINT)
curve_pubkey: Pubkey = get_associated_bonding_curve_address(mint_pubkey, PUMP_PROGRAM_ID)
print("Tracking bonding curve for:", mint_pubkey)
print("Curve address:", curve_pubkey, "\n")
async with AsyncClient(RPC_URL) as client:
while True:
try:
data = await get_account_data(client, curve_pubkey)
state = parse_curve_state(data)
print_curve_status(state)
except Exception as e:
print(f"⚠️ Error: {e}")
await asyncio.sleep(POLL_INTERVAL)
if __name__ == "__main__":
asyncio.run(track_curve())
+9 -7
View File
@@ -5,21 +5,23 @@ import struct
# Set the instruction name here
instruction_name = "account:BondingCurve"
def calculate_discriminator(instruction_name):
# Create a SHA256 hash object
sha = hashlib.sha256()
# Update the hash with the instruction name
sha.update(instruction_name.encode('utf-8'))
sha.update(instruction_name.encode("utf-8"))
# Get the first 8 bytes of the hash
discriminator_bytes = sha.digest()[:8]
# Convert the bytes to a 64-bit unsigned integer (little-endian)
discriminator = struct.unpack('<Q', discriminator_bytes)[0]
discriminator = struct.unpack("<Q", discriminator_bytes)[0]
return discriminator
# Calculate the discriminator for the specified instruction
discriminator = calculate_discriminator(instruction_name)
@@ -28,4 +30,4 @@ print(f"Discriminator for '{instruction_name}' instruction: {discriminator}")
# global:buy discriminator - 16927863322537952870
# global:sell discriminator - 12502976635542562355
# global:create discriminator - 8576854823835016728
# account:BondingCurve discriminator - 6966180631402821399
# account:BondingCurve discriminator - 6966180631402821399
@@ -1,96 +0,0 @@
import asyncio
import struct
from typing import Final
from construct import Struct, Int64ul, Flag
from solana.rpc.async_api import AsyncClient
from solders.pubkey import Pubkey
import argparse
import sys
import os
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from config import RPC_ENDPOINT, PUMP_PROGRAM
# Constants
EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack("<Q", 6966180631402821399)
class BondingCurveState:
_STRUCT = Struct(
"virtual_token_reserves" / Int64ul,
"virtual_sol_reserves" / Int64ul,
"real_token_reserves" / Int64ul,
"real_sol_reserves" / Int64ul,
"token_total_supply" / Int64ul,
"complete" / Flag
)
def __init__(self, data: bytes) -> None:
parsed = self._STRUCT.parse(data[8:])
self.__dict__.update(parsed)
def get_associated_bonding_curve_address(mint: Pubkey, program_id: Pubkey) -> tuple[Pubkey, int]:
"""
Derives the associated bonding curve address for a given mint
"""
return Pubkey.find_program_address(
[
b"bonding-curve",
bytes(mint)
],
program_id
)
async def get_bonding_curve_state(conn: AsyncClient, curve_address: Pubkey) -> BondingCurveState:
response = await conn.get_account_info(curve_address)
if not response.value or not response.value.data:
raise ValueError("Invalid curve state: No data")
data = response.value.data
if data[:8] != EXPECTED_DISCRIMINATOR:
raise ValueError("Invalid curve state discriminator")
return BondingCurveState(data)
async def check_token_status(mint_address: str) -> None:
try:
mint = Pubkey.from_string(mint_address)
# Get the associated bonding curve address
bonding_curve_address, bump = get_associated_bonding_curve_address(mint, PUMP_PROGRAM)
print("\nToken Status:")
print("-" * 50)
print(f"Token Mint: {mint}")
print(f"Associated Bonding Curve: {bonding_curve_address}")
print(f"Bump Seed: {bump}")
print("-" * 50)
# Check completion status
async with AsyncClient(RPC_ENDPOINT) as client:
try:
curve_state = await get_bonding_curve_state(client, bonding_curve_address)
print("\nBonding Curve Status:")
print("-" * 50)
print(f"Completion Status: {'Completed' if curve_state.complete else 'Not Completed'}")
if curve_state.complete:
print("\nNote: This bonding curve has completed and liquidity has been migrated to Raydium.")
print("-" * 50)
except ValueError as e:
print(f"\nError accessing bonding curve: {e}")
except ValueError as e:
print(f"\nError: Invalid address format - {e}")
except Exception as e:
print(f"\nUnexpected error: {e}")
def main():
parser = argparse.ArgumentParser(description='Check token bonding curve status')
parser.add_argument('mint_address', help='The token mint address')
args = parser.parse_args()
asyncio.run(check_token_status(args.mint_address))
if __name__ == "__main__":
main()
+87
View File
@@ -0,0 +1,87 @@
import asyncio
import os
from dotenv import load_dotenv
from solders.pubkey import Pubkey
from spl.token.instructions import BurnParams, CloseAccountParams, burn, close_account
from core.client import SolanaClient
from core.pubkeys import SystemAddresses
from core.wallet import Wallet
from utils.logger import get_logger
load_dotenv()
logger = get_logger(__name__)
RPC_ENDPOINT = os.getenv("SOLANA_NODE_RPC_ENDPOINT")
PRIVATE_KEY = os.getenv("SOLANA_PRIVATE_KEY")
# Update this address to MINT address of a token you want to close
MINT_ADDRESS = Pubkey.from_string("9WHpYbqG6LJvfCYfMjvGbyo1wHXgroCrixPb33s2pump")
async def close_account_if_exists(client: SolanaClient, wallet: Wallet, account: Pubkey, mint: Pubkey):
"""Safely close a token account if it exists and reclaim rent."""
try:
solana_client = await client.get_client()
info = await solana_client.get_account_info(account, encoding="base64") # base64 encoding for account data by deafult
# WARNING: This will permanently burn all tokens in the account before closing it
# Closing account is impossible if balance is positive
balance = await client.get_token_account_balance(account)
if balance > 0:
logger.info(f"Burning {balance} tokens from account {account}...")
burn_ix = burn(
BurnParams(
account=account,
mint=mint,
owner=wallet.pubkey,
amount=balance,
program_id=SystemAddresses.TOKEN_PROGRAM,
)
)
await client.build_and_send_transaction([burn_ix], wallet.keypair)
logger.info(f"Burned tokens from {account}")
# If account exists, attempt to close it
if info.value:
logger.info(f"Closing account: {account}")
close_params = CloseAccountParams(
account=account,
dest=wallet.pubkey,
owner=wallet.pubkey,
program_id=SystemAddresses.TOKEN_PROGRAM,
)
ix = close_account(close_params)
tx_sig = await client.build_and_send_transaction(
[ix],
wallet.keypair,
skip_preflight=True,
)
await client.confirm_transaction(tx_sig)
logger.info(f"Closed successfully: {account}")
else:
logger.info(f"Account does not exist or already closed: {account}")
except Exception as e:
logger.error(f"Error while processing account {account}: {e}")
async def main():
try:
client = SolanaClient(RPC_ENDPOINT)
wallet = Wallet(PRIVATE_KEY)
# Get user's ATA for the token
ata = wallet.get_associated_token_address(MINT_ADDRESS)
await close_account_if_exists(client, wallet, ata, MINT_ADDRESS)
except Exception as e:
logger.error(f"Unexpected error: {e}")
finally:
await client.close()
if __name__ == "__main__":
asyncio.run(main())
@@ -1,52 +1,52 @@
import sys
import os
import sys
from solders.pubkey import Pubkey
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from config import PUMP_PROGRAM
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from core.pubkeys import PumpAddresses, SystemAddresses
def get_bonding_curve_address(mint: Pubkey, program_id: Pubkey) -> tuple[Pubkey, int]:
"""
Derives the bonding curve address for a given mint
"""
return Pubkey.find_program_address(
[
b"bonding-curve",
bytes(mint)
],
program_id
)
return Pubkey.find_program_address([b"bonding-curve", bytes(mint)], program_id)
def find_associated_bonding_curve(mint: Pubkey, bonding_curve: Pubkey) -> Pubkey:
"""
Find the associated bonding curve for a given mint and bonding curve.
This uses the standard ATA derivation.
"""
from config import SYSTEM_TOKEN_PROGRAM as TOKEN_PROGRAM_ID
from config import SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM as ATA_PROGRAM_ID
derived_address, _ = Pubkey.find_program_address(
[
bytes(bonding_curve),
bytes(TOKEN_PROGRAM_ID),
bytes(mint),
bytes(SystemAddresses.TOKEN_PROGRAM),
bytes(mint),
],
ATA_PROGRAM_ID
SystemAddresses.ASSOCIATED_TOKEN_PROGRAM,
)
return derived_address
def main():
def main():
mint_address = input("Enter the token mint address: ")
try:
mint = Pubkey.from_string(mint_address)
bonding_curve_address, bump = get_bonding_curve_address(mint, PUMP_PROGRAM)
bonding_curve_address, bump = get_bonding_curve_address(
mint, PumpAddresses.PROGRAM
)
# Calculate the associated bonding curve
associated_bonding_curve = find_associated_bonding_curve(mint, bonding_curve_address)
associated_bonding_curve = find_associated_bonding_curve(
mint, bonding_curve_address
)
print("\nResults:")
print("-" * 50)
print(f"Token Mint: {mint}")
@@ -54,9 +54,10 @@ def main():
print(f"Associated Bonding Curve: {associated_bonding_curve}")
print(f"Bonding Curve Bump: {bump}")
print("-" * 50)
except ValueError as e:
print(f"Error: Invalid address format - {str(e)}")
if __name__ == "__main__":
main()
+85 -67
View File
@@ -1,59 +1,63 @@
import base64
import hashlib
import json
import struct
import hashlib
from solana.transaction import Transaction
from solders.transaction import VersionedTransaction
from solders.pubkey import Pubkey
import sys
from solders.transaction import Transaction, VersionedTransaction
def load_idl(file_path):
with open(file_path, 'r') as f:
with open(file_path) as f:
return json.load(f)
def load_transaction(file_path):
with open(file_path, 'r') as f:
with open(file_path) as f:
data = json.load(f)
return data
def decode_instruction(ix_data, ix_def):
args = {}
offset = 8 # Skip 8-byte discriminator
for arg in ix_def['args']:
if arg['type'] == 'u64':
value = struct.unpack_from('<Q', ix_data, offset)[0]
for arg in ix_def["args"]:
if arg["type"] == "u64":
value = struct.unpack_from("<Q", ix_data, offset)[0]
offset += 8
elif arg['type'] == 'publicKey':
value = ix_data[offset:offset+32].hex()
elif arg["type"] == "publicKey":
value = ix_data[offset : offset + 32].hex()
offset += 32
elif arg['type'] == 'string':
length = struct.unpack_from('<I', ix_data, offset)[0]
elif arg["type"] == "string":
length = struct.unpack_from("<I", ix_data, offset)[0]
offset += 4
value = ix_data[offset:offset+length].decode('utf-8')
value = ix_data[offset : offset + length].decode("utf-8")
offset += length
else:
raise ValueError(f"Unsupported type: {arg['type']}")
args[arg['name']] = value
args[arg["name"]] = value
return args
def calculate_discriminator(instruction_name):
sha = hashlib.sha256()
sha.update(instruction_name.encode('utf-8'))
sha.update(instruction_name.encode("utf-8"))
discriminator_bytes = sha.digest()[:8]
discriminator = struct.unpack('<Q', discriminator_bytes)[0]
discriminator = struct.unpack("<Q", discriminator_bytes)[0]
return discriminator
def decode_transaction(tx_data, idl):
decoded_instructions = []
# Decode the base64 transaction data
tx_data_decoded = base64.b64decode(tx_data['transaction'][0])
tx_data_decoded = base64.b64decode(tx_data["transaction"][0])
# Check if it's a versioned transaction
if tx_data.get('version') == 0:
if tx_data.get("version") == 0:
# Use solders library for versioned transactions
transaction = VersionedTransaction.from_bytes(tx_data_decoded)
instructions = transaction.message.instructions
@@ -61,72 +65,86 @@ def decode_transaction(tx_data, idl):
print("Versioned transaction detected")
else:
# Use legacy deserialization for older transactions
transaction = Transaction.deserialize(tx_data_decoded)
instructions = transaction.instructions
transaction = Transaction.from_bytes(tx_data_decoded)
instructions = transaction.message.instructions
account_keys = transaction.message.account_keys
print("Legacy transaction detected")
print(f"Number of instructions: {len(instructions)}")
for idx, ix in enumerate(instructions):
program_id = str(account_keys[ix.program_id_index])
print(f"\nInstruction {idx}:")
print(f"Program ID: {program_id}")
print(f"IDL program address: {idl['metadata']['address']}")
if program_id == idl['metadata']['address']:
if program_id == idl["metadata"]["address"]:
ix_data = bytes(ix.data)
discriminator = struct.unpack('<Q', ix_data[:8])[0]
discriminator = struct.unpack("<Q", ix_data[:8])[0]
print(f"Discriminator: {discriminator:016x}")
for idl_ix in idl['instructions']:
for idl_ix in idl["instructions"]:
idl_discriminator = calculate_discriminator(f"global:{idl_ix['name']}")
print(f"Checking against IDL instruction: {idl_ix['name']} with discriminator {idl_discriminator:016x}")
print(
f"Checking against IDL instruction: {idl_ix['name']} with discriminator {idl_discriminator:016x}"
)
if discriminator == idl_discriminator:
decoded_args = decode_instruction(ix_data, idl_ix)
accounts = [str(account_keys[acc_idx]) for acc_idx in ix.accounts]
decoded_instructions.append({
'name': idl_ix['name'],
'args': decoded_args,
'accounts': accounts,
'program': program_id
})
decoded_instructions.append(
{
"name": idl_ix["name"],
"args": decoded_args,
"accounts": accounts,
"program": program_id,
}
)
break
else:
decoded_instructions.append({
'name': 'Unknown',
'data': ix_data.hex(),
'accounts': [str(account_keys[acc_idx]) for acc_idx in ix.accounts],
'program': program_id
})
decoded_instructions.append(
{
"name": "Unknown",
"data": ix_data.hex(),
"accounts": [
str(account_keys[acc_idx]) for acc_idx in ix.accounts
],
"program": program_id,
}
)
else:
instruction_name = 'External'
if program_id == 'ComputeBudget111111111111111111111111111111':
if ix.data[:1] == b'\x03':
instruction_name = 'ComputeBudget: Set compute unit limit'
elif ix.data[:1] == b'\x02':
instruction_name = 'ComputeBudget: Set compute unit price'
elif program_id == 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL':
instruction_name = 'Associated Token Account: Create'
decoded_instructions.append({
'name': instruction_name,
'programId': program_id,
'data': bytes(ix.data).hex(),
'accounts': [str(account_keys[acc_idx]) for acc_idx in ix.accounts]
})
instruction_name = "External"
if program_id == "ComputeBudget111111111111111111111111111111":
if ix.data[:1] == b"\x03":
instruction_name = "ComputeBudget: Set compute unit limit"
elif ix.data[:1] == b"\x02":
instruction_name = "ComputeBudget: Set compute unit price"
elif program_id == "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL":
instruction_name = "Associated Token Account: Create"
decoded_instructions.append(
{
"name": instruction_name,
"programId": program_id,
"data": bytes(ix.data).hex(),
"accounts": [str(account_keys[acc_idx]) for acc_idx in ix.accounts],
}
)
return decoded_instructions
if len(sys.argv) != 2:
print("Usage: python decode_fromBlock.py <transaction_file_path>")
sys.exit(1)
tx_file_path = sys.argv[1]
idl = load_idl('../idl/pump_fun_idl.json')
tx_file_path = ""
if len(sys.argv) != 2:
tx_file_path = "learning-examples/blockSubscribe-transactions/raw_create_tx_from_blockSubscribe.json"
print(f"No path provided, using the path: {tx_file_path}")
else:
tx_file_path = sys.argv[1]
idl = load_idl("idl/pump_fun_idl.json")
tx_data = load_transaction(tx_file_path)
decoded_instructions = decode_transaction(tx_data, idl)
print(json.dumps(decoded_instructions, indent=2))
print(json.dumps(decoded_instructions, indent=2))
@@ -1,12 +1,14 @@
import json
import base64
import json
import struct
from construct import Struct, Int64ul, Flag
from construct import Flag, Int64ul, Struct
LAMPORTS_PER_SOL = 1_000_000_000
TOKEN_DECIMALS = 6
EXPECTED_DISCRIMINATOR = struct.pack("<Q", 6966180631402821399)
class BondingCurveState:
_STRUCT = Struct(
"virtual_token_reserves" / Int64ul,
@@ -14,18 +16,22 @@ class BondingCurveState:
"real_token_reserves" / Int64ul,
"real_sol_reserves" / Int64ul,
"token_total_supply" / Int64ul,
"complete" / Flag
"complete" / Flag,
)
def __init__(self, data: bytes) -> None:
parsed = self._STRUCT.parse(data[8:])
self.__dict__.update(parsed)
def calculate_bonding_curve_price(curve_state: BondingCurveState) -> float:
if curve_state.virtual_token_reserves <= 0 or curve_state.virtual_sol_reserves <= 0:
raise ValueError("Invalid reserve state")
return (curve_state.virtual_sol_reserves / LAMPORTS_PER_SOL) / (curve_state.virtual_token_reserves / 10 ** TOKEN_DECIMALS)
return (curve_state.virtual_sol_reserves / LAMPORTS_PER_SOL) / (
curve_state.virtual_token_reserves / 10**TOKEN_DECIMALS
)
def decode_bonding_curve_data(raw_data: str) -> BondingCurveState:
decoded_data = base64.b64decode(raw_data)
@@ -33,12 +39,13 @@ def decode_bonding_curve_data(raw_data: str) -> BondingCurveState:
raise ValueError("Invalid curve state discriminator")
return BondingCurveState(decoded_data)
# Load the JSON data
with open('raw_bondingCurve_from_getAccountInfo.json', 'r') as file:
with open("learning-examples/raw_bondingCurve_from_getAccountInfo.json", "r") as file:
json_data = json.load(file)
# Extract the base64 encoded data
encoded_data = json_data['result']['value']['data'][0]
encoded_data = json_data["result"]["value"]["data"][0]
# Decode the data
bonding_curve_state = decode_bonding_curve_data(encoded_data)
@@ -53,4 +60,4 @@ print(f" Real Token Reserves: {bonding_curve_state.real_token_reserves}")
print(f" Real SOL Reserves: {bonding_curve_state.real_sol_reserves}")
print(f" Token Total Supply: {bonding_curve_state.token_total_supply}")
print(f" Complete: {bonding_curve_state.complete}")
print(f"\nToken Price: {token_price_sol:.10f} SOL")
print(f"\nToken Price: {token_price_sol:.10f} SOL")
+36 -35
View File
@@ -1,32 +1,31 @@
import json
import base58
from solana.transaction import Transaction
from solders.pubkey import Pubkey
import struct
import sys
import base64
import sys
import base58
tx_file_path = ""
if len(sys.argv) != 2:
print("Usage: python decode_getTransaction.py <transaction_file_path>")
sys.exit(1)
tx_file_path = sys.argv[1]
tx_file_path = "learning-examples/raw_buy_tx_from_getTransaction.json"
print(f"No path provided, using the path: {tx_file_path}")
else:
tx_file_path = sys.argv[1]
# Load the IDL
with open('../idl/pump_fun_idl.json', 'r') as f:
with open("idl/pump_fun_idl.json") as f:
idl = json.load(f)
# Load the transaction log
with open(tx_file_path, 'r') as f:
with open(tx_file_path) as f:
tx_log = json.load(f)
# Extract the transaction data
tx_data = tx_log['result']['transaction']
tx_data = tx_log["result"]["transaction"]
print(json.dumps(tx_data, indent=2))
def decode_create_instruction(data):
# The Create instruction has 3 string arguments: name, symbol, uri
offset = 8 # Skip the 8-byte discriminator
@@ -34,59 +33,61 @@ def decode_create_instruction(data):
for _ in range(3):
length = struct.unpack_from("<I", data, offset)[0]
offset += 4
string_data = data[offset:offset+length].decode('utf-8')
string_data = data[offset : offset + length].decode("utf-8")
results.append(string_data)
offset += length
return {
"name": results[0],
"symbol": results[1],
"uri": results[2]
}
return {"name": results[0], "symbol": results[1], "uri": results[2]}
def decode_buy_instruction(data):
# Assuming the buy instruction has a u64 argument for amount
amount = struct.unpack_from("<Q", data, 8)[0]
return {"amount": amount}
def decode_instruction_data(instruction, accounts, data):
if instruction['name'] == 'create':
if instruction["name"] == "create":
return decode_create_instruction(data)
elif instruction['name'] == 'buy':
elif instruction["name"] == "buy":
return decode_buy_instruction(data)
else:
return f"Unhandled instruction type: {instruction['name']}"
def find_matching_instruction(accounts, data):
if 'instructions' not in idl:
if "instructions" not in idl:
print("Warning: No instructions found in IDL")
return None
for instruction in idl['instructions']:
if len(instruction['accounts']) == len(accounts):
for instruction in idl["instructions"]:
if len(instruction["accounts"]) == len(accounts):
return instruction
return None
# Parse the transaction
tx_message = tx_data['message']
instructions = tx_message['instructions']
tx_message = tx_data["message"]
instructions = tx_message["instructions"]
for ix in instructions:
program_id = ix.get('programId')
accounts = ix.get('accounts', [])
data = ix.get('data', '')
if 'parsed' in ix:
program_id = ix.get("programId")
accounts = ix.get("accounts", [])
data = ix.get("data", "")
if "parsed" in ix:
print(f"Parsed instruction: {ix['program']} - {ix['parsed']['type']}")
print(f"Info: {json.dumps(ix['parsed']['info'], indent=2)}")
elif program_id == idl['metadata']['address']:
elif program_id == idl["metadata"]["address"]:
matching_instruction = find_matching_instruction(accounts, data)
if matching_instruction:
decoded_data = decode_instruction_data(matching_instruction, accounts, base58.b58decode(data))
decoded_data = decode_instruction_data(
matching_instruction, accounts, base58.b58decode(data)
)
print(f"Instruction: {matching_instruction['name']}")
print(f"Decoded data: {decoded_data}")
print("\nAccounts:")
for i, account in enumerate(accounts):
account_info = matching_instruction['accounts'][i]
account_info = matching_instruction["accounts"][i]
print(f" {account_info['name']}: {account}")
else:
print(f"Unable to match instruction for program {program_id}")
@@ -97,4 +98,4 @@ for ix in instructions:
print("\nTransaction Information:")
print(f"Blockhash: {tx_message['recentBlockhash']}")
print(f"Fee payer: {tx_message['accountKeys'][0]['pubkey']}")
print(f"Signature: {tx_data['signatures'][0]}")
print(f"Signature: {tx_data['signatures'][0]}")
+19 -9
View File
@@ -1,15 +1,14 @@
import asyncio
import os
import struct
import sys
import os
from typing import Final
from construct import Struct, Int64ul, Flag
from construct import Flag, Int64ul, Struct
from solana.rpc.async_api import AsyncClient
from solders.pubkey import Pubkey
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from config import RPC_ENDPOINT
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
LAMPORTS_PER_SOL: Final[int] = 1_000_000_000
TOKEN_DECIMALS: Final[int] = 6
@@ -18,6 +17,9 @@ CURVE_ADDRESS: Final[str] = "6GXfUqrmPM4VdN1NoDZsE155jzRegJngZRjMkGyby7do"
# Here and later all the discriminators are precalculated. See learning-examples/discriminator.py
EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack("<Q", 6966180631402821399)
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
class BondingCurveState:
_STRUCT = Struct(
"virtual_token_reserves" / Int64ul,
@@ -25,15 +27,18 @@ class BondingCurveState:
"real_token_reserves" / Int64ul,
"real_sol_reserves" / Int64ul,
"token_total_supply" / Int64ul,
"complete" / Flag
"complete" / Flag,
)
def __init__(self, data: bytes) -> None:
parsed = self._STRUCT.parse(data[8:])
self.__dict__.update(parsed)
async def get_bonding_curve_state(conn: AsyncClient, curve_address: Pubkey) -> BondingCurveState:
response = await conn.get_account_info(curve_address)
async def get_bonding_curve_state(
conn: AsyncClient, curve_address: Pubkey
) -> BondingCurveState:
response = await conn.get_account_info(curve_address, encoding="base64")
if not response.value or not response.value.data:
raise ValueError("Invalid curve state: No data")
@@ -43,11 +48,15 @@ async def get_bonding_curve_state(conn: AsyncClient, curve_address: Pubkey) -> B
return BondingCurveState(data)
def calculate_bonding_curve_price(curve_state: BondingCurveState) -> float:
if curve_state.virtual_token_reserves <= 0 or curve_state.virtual_sol_reserves <= 0:
raise ValueError("Invalid reserve state")
return (curve_state.virtual_sol_reserves / LAMPORTS_PER_SOL) / (curve_state.virtual_token_reserves / 10 ** TOKEN_DECIMALS)
return (curve_state.virtual_sol_reserves / LAMPORTS_PER_SOL) / (
curve_state.virtual_token_reserves / 10**TOKEN_DECIMALS
)
async def main() -> None:
try:
@@ -63,5 +72,6 @@ async def main() -> None:
except Exception as e:
print(f"An unexpected error occurred: {e}")
if __name__ == "__main__":
asyncio.run(main())
asyncio.run(main())
@@ -0,0 +1,637 @@
"""
This script compares two methods of detecting migrations:
1. Migration program listener (listens Migration program) - detects markets via successful migration transactions
2. Direct market account listener (listens Pump Fun AMM program aka PumpSwap) - detects markets via program account subscription
The script tracks which method detects new markets first and provides detailed performance statistics.
Note: multiple endpoints available. Scroll down to change providers which you want to test.
"""
import asyncio
import base64
import json
import os
import struct
import time
import aiohttp
import base58
import websockets
from dotenv import load_dotenv
from solders.pubkey import Pubkey
load_dotenv()
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
MIGRATION_PROGRAM_ID = Pubkey.from_string("39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg")
PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
QUOTE_MINT_SOL = base58.b58encode(bytes(Pubkey.from_string("So11111111111111111111111111111111111111112"))).decode()
MARKET_DISCRIMINATOR = base58.b58encode(b'\xf1\x9am\x04\x11\xb1m\xbc').decode()
MARKET_ACCOUNT_LENGTH = 8 + 1 + 2 + 32 * 6 + 8 # total size of known market structure
class DetectionTracker:
"""Tracks and analyzes detection times for both methods across providers"""
def __init__(self):
self.migrations = {} # {base_mint: {provider: timestamp}}
self.markets = {} # {base_mint: {provider: timestamp}}
self.migration_messages = {} # {provider: count}
self.market_messages = {} # {provider: count}
self.start_time = time.time()
def add_migration(self, base_mint, provider, timestamp):
"""Record a migration detection event"""
if base_mint not in self.migrations:
self.migrations[base_mint] = {}
self.migrations[base_mint][provider] = timestamp
print(f"[MIGRATION] base_mint={base_mint} provider={provider} time={timestamp:.3f}")
def add_market(self, base_mint, provider, timestamp):
"""Record a market detection event"""
if base_mint not in self.markets:
self.markets[base_mint] = {}
self.markets[base_mint][provider] = timestamp
print(f"[MARKET] base_mint={base_mint} provider={provider} time={timestamp:.3f}")
def increment_migration_messages(self, provider):
"""Count WebSocket messages received by migration listener"""
if provider not in self.migration_messages:
self.migration_messages[provider] = 0
self.migration_messages[provider] += 1
def increment_market_messages(self, provider):
"""Count WebSocket messages received by market listener"""
if provider not in self.market_messages:
self.market_messages[provider] = 0
self.market_messages[provider] += 1
def print_summary(self):
"""Print detailed summary statistics of the comparison test"""
test_duration = time.time() - self.start_time
# Count total messages
total_migration_messages = sum(self.migration_messages.values())
total_market_messages = sum(self.market_messages.values())
print("\n=== Test Summary ===")
print(f"Test duration: {test_duration:.2f} seconds")
print(f"WebSocket messages received: {total_migration_messages + total_market_messages}")
print(f" - Migration events: {total_migration_messages}")
print(f" - Market events: {total_market_messages}")
# Count unique tokens detected by each method
unique_migrations = set(self.migrations.keys())
unique_markets = set(self.markets.keys())
common_tokens = unique_migrations & unique_markets
print(f"Tokens detected: {len(unique_migrations | unique_markets)}")
print(f" - Migration events: {len(unique_migrations)}")
print(f" - Market events: {len(unique_markets)}")
print(f" - Detected in both: {len(common_tokens)}\n")
print("=== Provider Message Counts ===")
print("Provider | Migration Messages | Market Messages | Total Messages")
print("-" * 80)
all_providers = set(self.migration_messages.keys()) | set(self.market_messages.keys())
for provider in sorted(all_providers):
migration_count = self.migration_messages.get(provider, 0)
market_count = self.market_messages.get(provider, 0)
total = migration_count + market_count
print(f"{provider:<22} | {migration_count:<18} | {market_count:<14} | {total}")
print()
print("=== Migration Event Provider Performance ===")
self._print_provider_performance(self.migrations)
print("\n=== Market Event Provider Performance ===")
self._print_provider_performance(self.markets)
# Compare detection methods for tokens detected by both
if common_tokens:
print("\n=== Detection Timing Comparison: Migration vs Market ===")
print("Base Mint | First Detection Method | First Provider | Time Delta (ms)")
print("-" * 100)
migration_first = 0
market_first = 0
total_delta_ms = 0
for base_mint in sorted(common_tokens):
# Find earliest time for each method
migration_time = min(self.migrations[base_mint].values()) if base_mint in self.migrations else float('inf')
market_time = min(self.markets[base_mint].values()) if base_mint in self.markets else float('inf')
# Find provider with earliest time for each method
migration_provider = None
if base_mint in self.migrations:
migration_provider = min(self.migrations[base_mint].items(), key=lambda x: x[1])[0]
market_provider = None
if base_mint in self.markets:
market_provider = min(self.markets[base_mint].items(), key=lambda x: x[1])[0]
delta_ms = abs(migration_time - market_time) * 1000
total_delta_ms += delta_ms
if migration_time < market_time:
first_method = "Migration"
first_provider = migration_provider
migration_first += 1
else:
first_method = "Market"
first_provider = market_provider
market_first += 1
print(f"{base_mint} | {first_method:<21} | {first_provider:<14} | {delta_ms:8.1f}")
# Print statistics summary
if common_tokens:
avg_delta_ms = total_delta_ms / len(common_tokens)
print("\nSummary statistics:")
print(f" - Migration detected first: {migration_first}/{len(common_tokens)} ({migration_first/len(common_tokens)*100:.1f}%)")
print(f" - Market detected first: {market_first}/{len(common_tokens)} ({market_first/len(common_tokens)*100:.1f}%)")
print(f" - Average timing difference: {avg_delta_ms:.1f} ms")
def _print_provider_performance(self, events_dict):
"""Print performance metrics for providers using a specific detection method"""
# Count how many times each provider was first
first_count = {}
total_events = 0
for base_mint, providers in events_dict.items():
total_events += 1
if not providers:
continue
# Find the fastest provider for this event
fastest_provider = min(providers.items(), key=lambda x: x[1])[0]
if fastest_provider not in first_count:
first_count[fastest_provider] = 0
first_count[fastest_provider] += 1
if not first_count:
print("No events detected")
return
# Print rankings
print("Provider | First Detections | Percentage")
print("-" * 60)
for provider, count in sorted(first_count.items(), key=lambda x: x[1], reverse=True):
percentage = (count / total_events) * 100 if total_events > 0 else 0
print(f"{provider:<22} | {count:<16} | {percentage:.1f}%")
# Calculate average latency between providers
self._print_provider_latency_matrix(events_dict)
def _print_provider_latency_matrix(self, events_dict):
"""Print a matrix of average latency between providers"""
# Get unique providers
all_providers = set()
for providers_data in events_dict.values():
all_providers.update(providers_data.keys())
if len(all_providers) <= 1:
return
providers_list = sorted(all_providers)
print("\nAverage Latency Matrix (ms):")
# Print header
header = " |"
for provider in providers_list:
header += f" {provider[:8]:>8} |"
print(header)
print("-" * len(header))
# Calculate and print latency matrix
for provider1 in providers_list:
row = f"{provider1[:8]:>8} |"
for provider2 in providers_list:
if provider1 == provider2:
row += " — |"
continue
# Calculate average latency
latencies = []
for base_mint, providers_data in events_dict.items():
if provider1 in providers_data and provider2 in providers_data:
latency_ms = (providers_data[provider2] - providers_data[provider1]) * 1000
latencies.append(latency_ms)
if latencies:
avg_latency = sum(latencies) / len(latencies)
row += f" {avg_latency:>+7.1f} |"
else:
row += " ? |"
print(row)
# ============ MARKET DETECTION METHODS ============
async def fetch_existing_market_pubkeys():
"""
Fetch existing AMM market accounts from the blockchain
Used to filter out already existing markets when detecting new ones
"""
headers = {"Content-Type": "application/json"}
body = {
"jsonrpc": "2.0",
"id": 1,
"method": "getProgramAccounts",
"params": [
str(PUMP_AMM_PROGRAM_ID),
{
"encoding": "base64",
"commitment": "processed",
"filters": [
{"dataSize": MARKET_ACCOUNT_LENGTH},
{"memcmp": {"offset": 0, "bytes": MARKET_DISCRIMINATOR}},
{"memcmp": {"offset": 75, "bytes": QUOTE_MINT_SOL}}
]
}
]
}
async with aiohttp.ClientSession() as session:
async with session.post(RPC_ENDPOINT, headers=headers, json=body) as resp:
res = await resp.json()
return {account["pubkey"] for account in res.get("result", [])}
def parse_market_account_data(data):
"""
Parse binary market account data into a structured format
This function matches the parser from the market listener script
"""
parsed_data = {}
offset = 8 # Skip discriminator
fields = [
("pool_bump", "u8"),
("index", "u16"),
("creator", "pubkey"),
("base_mint", "pubkey"),
("quote_mint", "pubkey"),
("lp_mint", "pubkey"),
("pool_base_token_account", "pubkey"),
("pool_quote_token_account", "pubkey"),
("lp_supply", "u64"),
]
try:
for field_name, field_type in fields:
if field_type == "pubkey":
value = data[offset:offset + 32]
parsed_data[field_name] = base58.b58encode(value).decode("utf-8")
offset += 32
elif field_type in {"u64", "i64"}:
value = struct.unpack("<Q", data[offset:offset + 8])[0] if field_type == "u64" else struct.unpack("<q", data[offset:offset + 8])[0]
parsed_data[field_name] = value
offset += 8
elif field_type == "u16":
value = struct.unpack("<H", data[offset:offset + 2])[0]
parsed_data[field_name] = value
offset += 2
elif field_type == "u8":
value = data[offset]
parsed_data[field_name] = value
offset += 1
except Exception as e:
print(f"[ERROR] Failed to parse market data: {e}")
return parsed_data
def parse_migrate_instruction(data):
"""
Parse binary migration instruction data into a structured format
This function matches the parser from the migration listener script
"""
if len(data) < 8:
print(f"[ERROR] Data length too short: {len(data)} bytes")
return None
offset = 8 # Skip discriminator
parsed_data = {}
fields = [
("timestamp", "i64"),
("index", "u16"),
("creator", "pubkey"),
("baseMint", "pubkey"),
("quoteMint", "pubkey"),
("baseMintDecimals", "u8"),
("quoteMintDecimals", "u8"),
("baseAmountIn", "u64"),
("quoteAmountIn", "u64"),
("poolBaseAmount", "u64"),
("poolQuoteAmount", "u64"),
("minimumLiquidity", "u64"),
("initialLiquidity", "u64"),
("lpTokenAmountOut", "u64"),
("poolBump", "u8"),
("pool", "pubkey"),
("lpMint", "pubkey"),
("userBaseTokenAccount", "pubkey"),
("userQuoteTokenAccount", "pubkey"),
]
try:
for field_name, field_type in fields:
if field_type == "pubkey":
value = data[offset:offset + 32]
parsed_data[field_name] = base58.b58encode(value).decode("utf-8")
offset += 32
elif field_type in {"u64", "i64"}:
value = struct.unpack("<Q", data[offset:offset + 8])[0] if field_type == "u64" else struct.unpack("<q", data[offset:offset + 8])[0]
parsed_data[field_name] = value
offset += 8
elif field_type == "u16":
value = struct.unpack("<H", data[offset:offset + 2])[0]
parsed_data[field_name] = value
offset += 2
elif field_type == "u8":
value = data[offset]
parsed_data[field_name] = value
offset += 1
return parsed_data
except Exception as e:
print(f"[ERROR] Failed to parse migration data at offset {offset}: {e}")
return None
def is_transaction_successful(logs):
"""Check if a transaction was successful based on log messages"""
for log in logs:
if "AnchorError thrown" in log or "Error" in log:
return False
return True
# ============ WEBSOCKET LISTENERS ============
async def listen_for_migrations(wss_url, provider_name, tracker, known_events=None):
"""
Listen for migration instructions via WebSocket
Args:
wss_url: WebSocket URL to connect to
provider_name: Name of the RPC provider
tracker: DetectionTracker instance to record events
known_events: Set of already known (provider, base_mint) tuples to prevent duplicates
"""
if known_events is None:
known_events = set()
while True:
try:
print(f"[INFO] Connecting migration listener to {provider_name}...")
async with websockets.connect(wss_url) as websocket:
# Subscribe to logs mentioning the migration program
subscription_message = json.dumps({
"jsonrpc": "2.0",
"id": 1,
"method": "logsSubscribe",
"params": [
{"mentions": [str(MIGRATION_PROGRAM_ID)]},
{"commitment": "processed"},
],
})
await websocket.send(subscription_message)
await websocket.recv() # Get subscription confirmation
print(f"[INFO] Migration listener active for {provider_name}")
while True:
try:
# Receive WebSocket message
response = await websocket.recv()
data = json.loads(response)
tracker.increment_migration_messages(provider_name)
# Check if it's a notification and not something else
if data.get("method") != "logsNotification":
continue
# Get transaction logs
log_data = data["params"]["result"]["value"]
logs = log_data.get("logs", [])
# Skip failed transactions
if not is_transaction_successful(logs):
continue
# Skip if not a Migrate instruction
if not any("Instruction: Migrate" in log for log in logs):
continue
# Skip already migrated curves
if any("already migrated" in log for log in logs):
continue
# Search for Program data in logs
for log in logs:
if log.startswith("Program data:"):
try:
# Decode and parse the instruction data
data = base64.b64decode(log.split(": ")[1])
parsed = parse_migrate_instruction(data)
if parsed and "baseMint" in parsed:
base_mint = parsed["baseMint"]
# Only track the timestamp for the first time we see this event
# from this provider, but still count messages
if (provider_name, base_mint) not in known_events:
ts = time.time()
tracker.add_migration(base_mint, provider_name, ts)
known_events.add((provider_name, base_mint))
break
except Exception as e:
print(f"[ERROR] Failed to decode Program data: {e}")
except Exception as e:
print(f"[ERROR] Migration listener for {provider_name}: {e}")
except Exception as e:
print(f"[ERROR] Connection error in migration listener for {provider_name}: {e}")
print("[INFO] Reconnecting in 5 seconds...")
await asyncio.sleep(5)
async def listen_for_markets(wss_url, provider_name, tracker, known_markets):
"""
Listen for new market accounts via WebSocket
Args:
wss_url: WebSocket URL to connect to
provider_name: Name of the RPC provider
tracker: DetectionTracker instance to record events
known_markets: Set of already known market pubkeys to prevent duplicates
"""
while True:
try:
print(f"[INFO] Connecting market listener to {provider_name}...")
async with websockets.connect(wss_url) as websocket:
# Subscribe to program account changes
sub_msg = json.dumps({
"jsonrpc": "2.0",
"id": 1,
"method": "programSubscribe",
"params": [
str(PUMP_AMM_PROGRAM_ID),
{
"commitment": "processed",
"encoding": "base64",
"filters": [
{"dataSize": MARKET_ACCOUNT_LENGTH},
{"memcmp": {"offset": 0, "bytes": MARKET_DISCRIMINATOR}},
{"memcmp": {"offset": 75, "bytes": QUOTE_MINT_SOL}}
]
}
]
})
await websocket.send(sub_msg)
await websocket.recv() # Get subscription confirmation
print(f"[INFO] Market listener active for {provider_name}")
# Track events already seen by this provider
provider_known = set()
while True:
try:
# Receive WebSocket message
msg = await websocket.recv()
data = json.loads(msg)
tracker.increment_market_messages(provider_name)
# Check if it's a notification and not something else
if data.get("method") != "programNotification":
continue
# Extract account information
message_value = data["params"]["result"]["value"]
pubkey = message_value["pubkey"]
raw_account_data = message_value["account"].get("data", [None])[0]
# Skip if we've already processed this market (either globally or for this provider)
if pubkey in known_markets or pubkey in provider_known:
continue
provider_known.add(pubkey)
# Skip if there's no data
if not raw_account_data:
print("[ERROR] Account data is empty")
continue
try:
# Decode and parse the account data
account_data = base64.b64decode(raw_account_data)
parsed = parse_market_account_data(account_data)
# Skip user-created markets (they are on-curve)
if parsed.get("creator") and Pubkey.from_string(parsed.get("creator")).is_on_curve():
continue # skip user-created pool
# Record the market detection
base_mint = parsed.get("base_mint")
if base_mint:
ts = time.time()
tracker.add_market(base_mint, provider_name, ts)
# Add to the shared known markets to avoid duplicate processing
known_markets.add(pubkey)
except Exception as e:
print(f"[ERROR] Failed to decode account: {e}")
except Exception as e:
print(f"[ERROR] Market listener for {provider_name}: {e}")
except Exception as e:
print(f"[ERROR] Connection error in market listener for {provider_name}: {e}")
print("[INFO] Reconnecting in 5 seconds...")
await asyncio.sleep(5)
# ============ MAIN TEST RUNNER ============
async def run_comparison_test(migration_wss_endpoints, market_wss_endpoints, test_duration=600):
"""
Run the comparison test with multiple WebSocket endpoints
Args:
migration_wss_endpoints: Dict of {provider_name: wss_url} for migration listeners
market_wss_endpoints: Dict of {provider_name: wss_url} for market listeners
test_duration: How long to run the test in seconds (default: 10 minutes)
"""
# Initialize our tracker and fetch existing markets to avoid duplicates
tracker = DetectionTracker()
known_markets = await fetch_existing_market_pubkeys()
print(f"[INFO] Loaded {len(known_markets)} existing markets")
known_migration_events = set()
tasks = []
# Start migration listeners
for provider_name, wss_url in migration_wss_endpoints.items():
print(f"[INFO] Starting migration listener for {provider_name}")
task = asyncio.create_task(
listen_for_migrations(wss_url, provider_name, tracker, known_migration_events)
)
tasks.append(task)
# Start market listeners
for provider_name, wss_url in market_wss_endpoints.items():
print(f"[INFO] Starting market listener for {provider_name}")
task = asyncio.create_task(
listen_for_markets(wss_url, provider_name, tracker, known_markets)
)
tasks.append(task)
# Run for specified duration
print(f"[INFO] Test running for {test_duration} seconds...")
await asyncio.sleep(test_duration)
for task in tasks:
task.cancel()
await asyncio.gather(*tasks, return_exceptions=True)
tracker.print_summary()
if __name__ == "__main__":
# Read providers from environment variables
# You can add more providers by adding additional environment variables
migration_providers = {
"chainstack": os.environ.get("SOLANA_NODE_WSS_ENDPOINT"),
"provider_2": os.environ.get("SOLANA_NODE_WSS_ENDPOINT_2"),
# Add more providers to .env as needed
}
market_providers = {
"chainstack": os.environ.get("SOLANA_NODE_WSS_ENDPOINT"),
"provider_2": os.environ.get("SOLANA_NODE_WSS_ENDPOINT_2"),
# Add more providers to .env as needed
}
# Filter out any providers with missing endpoints
migration_providers = {name: url for name, url in migration_providers.items() if url}
market_providers = {name: url for name, url in market_providers.items() if url}
# Get test duration from environment or use default (10 minutes)
TEST_DURATION = int(os.environ.get("TEST_DURATION", 600))
print(f"[INFO] Starting Solana detector comparison test for {TEST_DURATION} seconds")
print(f"[INFO] Migration providers: {', '.join(migration_providers.keys())}")
print(f"[INFO] Market providers: {', '.join(market_providers.keys())}")
# Run the test
asyncio.run(run_comparison_test(migration_providers, market_providers, test_duration=TEST_DURATION))
@@ -0,0 +1,113 @@
import asyncio
import json
import os
import websockets
from dotenv import load_dotenv
from solders.pubkey import Pubkey
load_dotenv()
WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
PUMP_MIGRATOR_ID = Pubkey.from_string("39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg")
def process_initialize2_transaction(data):
"""Process and decode an initialize2 transaction"""
try:
signature = data["transaction"]["signatures"][0]
account_keys = data["transaction"]["message"]["accountKeys"]
# Check raydium_amm_idl.json for the account keys
# The token address is typically the 19th account (index 18)
# The liquidity pool address is typically the 3rd account (index 2)
if len(account_keys) > 18:
token_address = account_keys[18]
liquidity_address = account_keys[2]
print(f"\nSignature: {signature}")
print(f"Token Address: {token_address}")
print(f"Liquidity Address: {liquidity_address}")
print("=" * 50)
else:
print(f"\nError: Not enough account keys (found {len(account_keys)})")
except Exception as e:
print(f"\nError: {e!s}")
async def listen_for_events():
while True:
try:
async with websockets.connect(WSS_ENDPOINT) as websocket:
subscription_message = json.dumps(
{
"jsonrpc": "2.0",
"id": 1,
"method": "blockSubscribe",
"params": [
{
"mentionsAccountOrProgram": str(
PUMP_MIGRATOR_ID
)
},
{
"commitment": "confirmed",
"encoding": "json",
"showRewards": False,
"transactionDetails": "full",
"maxSupportedTransactionVersion": 0,
},
],
}
)
await websocket.send(subscription_message)
response = await websocket.recv()
print(f"Subscription response: {response}")
print("\nListening for Raydium pool initialization events...")
while True:
try:
response = await asyncio.wait_for(websocket.recv(), timeout=30)
data = json.loads(response)
if "method" in data and data["method"] == "blockNotification":
if "params" in data and "result" in data["params"]:
block_data = data["params"]["result"]
if (
"value" in block_data
and "block" in block_data["value"]
):
block = block_data["value"]["block"]
if "transactions" in block:
for tx in block["transactions"]:
logs = tx.get("meta", {}).get(
"logMessages", []
)
# Check for initialize2 instruction
for log in logs:
if (
"Program log: initialize2: InitializeInstruction2"
in log
):
print(
"Found initialize2 instruction!"
)
process_initialize2_transaction(tx)
break
except TimeoutError:
print("\nChecking connection...")
print("Connection alive")
continue
except Exception as e:
print(f"\nConnection error: {e!s}")
print("Retrying in 5 seconds...")
await asyncio.sleep(5)
if __name__ == "__main__":
asyncio.run(listen_for_events())
@@ -0,0 +1,169 @@
"""
Listens for 'Migrate' instructions from a Solana migration program via WebSocket.
Parses and logs transaction details (e.g., mint, liquidity, token accounts) for successful migrations.
Note: skips transactions with truncated logs (no Program data in the logs -> no parsed data).
To cover those cases, please use an additional RPC call (get transaction data) or additional listener not based on logs.
"""
import asyncio
import base64
import json
import os
import struct
import base58
import websockets
from dotenv import load_dotenv
from solders.pubkey import Pubkey
load_dotenv()
WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
MIGRATION_PROGRAM_ID = Pubkey.from_string("39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg")
def parse_migrate_instruction(data):
if len(data) < 8:
print(f"[ERROR] Data length too short: {len(data)} bytes")
return None
offset = 8
parsed_data = {}
fields = [
("timestamp", "i64"),
("index", "u16"),
("creator", "publicKey"),
("baseMint", "publicKey"),
("quoteMint", "publicKey"),
("baseMintDecimals", "u8"),
("quoteMintDecimals", "u8"),
("baseAmountIn", "u64"),
("quoteAmountIn", "u64"),
("poolBaseAmount", "u64"),
("poolQuoteAmount", "u64"),
("minimumLiquidity", "u64"),
("initialLiquidity", "u64"),
("lpTokenAmountOut", "u64"),
("poolBump", "u8"),
("pool", "publicKey"),
("lpMint", "publicKey"),
("userBaseTokenAccount", "publicKey"),
("userQuoteTokenAccount", "publicKey"),
]
try:
for field_name, field_type in fields:
if field_type == "publicKey":
value = data[offset:offset + 32]
parsed_data[field_name] = base58.b58encode(value).decode("utf-8")
offset += 32
elif field_type in {"u64", "i64"}:
value = struct.unpack("<Q", data[offset:offset + 8])[0] if field_type == "u64" else struct.unpack("<q", data[offset:offset + 8])[0]
parsed_data[field_name] = value
offset += 8
elif field_type == "u16":
value = struct.unpack("<H", data[offset:offset + 2])[0]
parsed_data[field_name] = value
offset += 2
elif field_type == "u8":
value = data[offset]
parsed_data[field_name] = value
offset += 1
return parsed_data
except Exception as e:
print(f"[ERROR] Failed to parse data at offset {offset}: {e}")
return None
def is_transaction_successful(logs):
for log in logs:
if "AnchorError thrown" in log or "Error" in log:
print(f"[ERROR] Transaction failed: {log}")
return False
return True
def print_transaction_details(log_data):
logs = log_data.get("logs", [])
parsed_data = {}
for log in logs:
if log.startswith("Program data:"):
try:
data = base64.b64decode(log.split(": ")[1])
parsed_data = parse_migrate_instruction(data)
if parsed_data:
print("[INFO] Parsed from Program data:")
for key, value in parsed_data.items():
print(f" {key}: {value}")
except Exception as e:
print(f"[ERROR] Failed to decode Program data: {e}")
if not parsed_data:
print("[ERROR] Failed to parse migration data: parsed data is empty")
async def listen_for_migrations():
while True:
try:
print("\n[INFO] Connecting to WebSocket ...")
async with websockets.connect(WSS_ENDPOINT) as websocket:
subscription_message = json.dumps(
{
"jsonrpc": "2.0",
"id": 1,
"method": "logsSubscribe",
"params": [
{"mentions": [str(MIGRATION_PROGRAM_ID)]},
{"commitment": "processed"},
],
}
)
await websocket.send(subscription_message)
print(f"[INFO] Listening for migration instructions from program: {MIGRATION_PROGRAM_ID}")
response = await websocket.recv()
print(f"[INFO] Subscription response: {response}")
while True:
try:
response = await asyncio.wait_for(websocket.recv(), timeout=60)
data = json.loads(response)
if "method" in data and data["method"] == "logsNotification":
log_data = data["params"]["result"]["value"]
logs = log_data.get("logs", [])
signature = log_data.get('signature', 'N/A')
print(f"\n[INFO] Transaction signature: {signature}")
if is_transaction_successful(logs):
if not any("Program log: Instruction: Migrate" in log for log in logs):
print("[INFO] Skipping: no migrate instruction")
continue
if any("Program log: Bonding curve already migrated" in log for log in logs):
print("[INFO] Skipping: bonding curve already migrated")
continue
print("[INFO] Processing migration instruction...")
print_transaction_details(log_data)
else:
print("[INFO] Skipping failed transaction.")
except TimeoutError:
print("[INFO] Timeout waiting for WebSocket message, retrying...")
except Exception as e:
print(f"[ERROR] An error occurred: {e}")
break
except Exception as e:
print(f"[ERROR] Connection error: {e}")
print("[INFO] Reconnecting in 5 seconds...")
await asyncio.sleep(5)
if __name__ == "__main__":
asyncio.run(listen_for_migrations())
@@ -0,0 +1,169 @@
"""
Monitors Solana for new Pump AMM markets via WebSocket.
Fetches existing markets to filter out already existing ones, parses market account data (e.g., mints, token accounts, creator),
and excludes user-created markets. May also detect non-migration-based markets (if they created by a program).
Note: this method consumes HUGE AMOUNT OF MESSAGES from a WebSocket.
"""
import asyncio
import base64
import json
import os
import struct
import aiohttp
import base58
import websockets
from dotenv import load_dotenv
from solders.pubkey import Pubkey
load_dotenv()
WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
MARKET_ACCOUNT_LENGTH = 8 + 1 + 2 + 32 * 6 + 8 # total size of known market structure
MARKET_DISCRIMINATOR = base58.b58encode(b'\xf1\x9am\x04\x11\xb1m\xbc').decode()
QUOTE_MINT_SOL = base58.b58encode(bytes(Pubkey.from_string("So11111111111111111111111111111111111111112"))).decode()
async def fetch_existing_market_pubkeys():
headers = {"Content-Type": "application/json"}
body = {
"jsonrpc": "2.0",
"id": 1,
"method": "getProgramAccounts",
"params": [
str(PUMP_AMM_PROGRAM_ID),
{
"encoding": "base64",
"commitment": "processed",
"filters": [
{"dataSize": MARKET_ACCOUNT_LENGTH},
{"memcmp": {"offset": 0, "bytes": MARKET_DISCRIMINATOR}},
{"memcmp": {"offset": 75, "bytes": QUOTE_MINT_SOL}}
]
}
]
}
async with aiohttp.ClientSession() as session:
async with session.post(RPC_ENDPOINT, headers=headers, json=body) as resp:
res = await resp.json()
return {account["pubkey"] for account in res.get("result", [])}
def parse_market_account_data(data):
parsed_data = {}
offset = 8 # Discriminator
fields = [
("pool_bump", "u8"),
("index", "u16"),
("creator", "pubkey"),
("base_mint", "pubkey"),
("quote_mint", "pubkey"),
("lp_mint", "pubkey"),
("pool_base_token_account", "pubkey"),
("pool_quote_token_account", "pubkey"),
("lp_supply", "u64"),
]
try:
for field_name, field_type in fields:
if field_type == "pubkey":
value = data[offset:offset + 32]
parsed_data[field_name] = base58.b58encode(value).decode("utf-8")
offset += 32
elif field_type in {"u64", "i64"}:
value = struct.unpack("<Q", data[offset:offset + 8])[0] if field_type == "u64" else struct.unpack("<q", data[offset:offset + 8])[0]
parsed_data[field_name] = value
offset += 8
elif field_type == "u16":
value = struct.unpack("<H", data[offset:offset + 2])[0]
parsed_data[field_name] = value
offset += 2
elif field_type == "u8":
value = data[offset]
parsed_data[field_name] = value
offset += 1
except Exception as e:
print(f"[ERROR] Failed to parse market data: {e}")
return parsed_data
async def listen_new_markets():
known_pubkeys = await fetch_existing_market_pubkeys()
print(f"[INFO] Loaded {len(known_pubkeys)} existing markets")
while True:
try:
print("[INFO] Connecting to WebSocket...")
async with websockets.connect(WSS_ENDPOINT) as ws:
sub_msg = json.dumps({
"jsonrpc": "2.0",
"id": 1,
"method": "programSubscribe",
"params": [
str(PUMP_AMM_PROGRAM_ID),
{
"commitment": "processed",
"encoding": "base64",
"filters": [
{"dataSize": MARKET_ACCOUNT_LENGTH},
{"memcmp": {"offset": 0, "bytes": MARKET_DISCRIMINATOR}},
{"memcmp": {"offset": 75, "bytes": QUOTE_MINT_SOL}}
]
}
]
})
await ws.send(sub_msg)
print(f"[INFO] Subscribed to: {PUMP_AMM_PROGRAM_ID}")
while True:
msg = await ws.recv()
data = json.loads(msg)
if "method" in data and data["method"] == "programNotification":
message_value = data["params"]["result"]["value"]
pubkey = message_value["pubkey"]
raw_account_data = message_value["account"].get("data", [None])[0]
slot = data["params"]["result"]["context"]["slot"]
if pubkey in known_pubkeys:
#print("[INFO] Skipping already existed market...")
continue
if not raw_account_data:
print("[ERROR] Account data is empty")
continue
try:
account_data = base64.b64decode(raw_account_data)
parsed = parse_market_account_data(account_data)
if Pubkey.from_string(parsed.get("creator", "")).is_on_curve():
print("[INFO] Skipping user-created market...")
continue # skip user-created pool
print("\n[INFO] New market account detected:")
print(f" pubkey: {pubkey}")
print(f" slot: {slot}")
for k, v in parsed.items():
print(f" {k}: {v}")
known_pubkeys.add(pubkey)
except Exception as e:
print(f"[ERROR] Failed to decode account: {e}")
except Exception as e:
print(f"[ERROR] Connection error: {e}")
print("[INFO] Reconnecting in 5 seconds...")
await asyncio.sleep(5)
if __name__ == "__main__":
asyncio.run(listen_new_markets())
File diff suppressed because one or more lines are too long
@@ -0,0 +1,485 @@
import solana_storage_pb2 as _solana_storage_pb2
from google.protobuf.internal import containers as _containers
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
from solana_storage_pb2 import ConfirmedBlock as ConfirmedBlock
from solana_storage_pb2 import ConfirmedTransaction as ConfirmedTransaction
from solana_storage_pb2 import Transaction as Transaction
from solana_storage_pb2 import Message as Message
from solana_storage_pb2 import MessageHeader as MessageHeader
from solana_storage_pb2 import MessageAddressTableLookup as MessageAddressTableLookup
from solana_storage_pb2 import TransactionStatusMeta as TransactionStatusMeta
from solana_storage_pb2 import TransactionError as TransactionError
from solana_storage_pb2 import InnerInstructions as InnerInstructions
from solana_storage_pb2 import InnerInstruction as InnerInstruction
from solana_storage_pb2 import CompiledInstruction as CompiledInstruction
from solana_storage_pb2 import TokenBalance as TokenBalance
from solana_storage_pb2 import UiTokenAmount as UiTokenAmount
from solana_storage_pb2 import ReturnData as ReturnData
from solana_storage_pb2 import Reward as Reward
from solana_storage_pb2 import Rewards as Rewards
from solana_storage_pb2 import UnixTimestamp as UnixTimestamp
from solana_storage_pb2 import BlockHeight as BlockHeight
from solana_storage_pb2 import NumPartitions as NumPartitions
from solana_storage_pb2 import RewardType as RewardType
DESCRIPTOR: _descriptor.FileDescriptor
Unspecified: _solana_storage_pb2.RewardType
Fee: _solana_storage_pb2.RewardType
Rent: _solana_storage_pb2.RewardType
Staking: _solana_storage_pb2.RewardType
Voting: _solana_storage_pb2.RewardType
class CommitmentLevel(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
PROCESSED: _ClassVar[CommitmentLevel]
CONFIRMED: _ClassVar[CommitmentLevel]
FINALIZED: _ClassVar[CommitmentLevel]
FIRST_SHRED_RECEIVED: _ClassVar[CommitmentLevel]
COMPLETED: _ClassVar[CommitmentLevel]
CREATED_BANK: _ClassVar[CommitmentLevel]
DEAD: _ClassVar[CommitmentLevel]
PROCESSED: CommitmentLevel
CONFIRMED: CommitmentLevel
FINALIZED: CommitmentLevel
FIRST_SHRED_RECEIVED: CommitmentLevel
COMPLETED: CommitmentLevel
CREATED_BANK: CommitmentLevel
DEAD: CommitmentLevel
class SubscribeRequest(_message.Message):
__slots__ = ("accounts", "slots", "transactions", "transactions_status", "blocks", "blocks_meta", "entry", "commitment", "accounts_data_slice", "ping")
class AccountsEntry(_message.Message):
__slots__ = ("key", "value")
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: SubscribeRequestFilterAccounts
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterAccounts, _Mapping]] = ...) -> None: ...
class SlotsEntry(_message.Message):
__slots__ = ("key", "value")
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: SubscribeRequestFilterSlots
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterSlots, _Mapping]] = ...) -> None: ...
class TransactionsEntry(_message.Message):
__slots__ = ("key", "value")
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: SubscribeRequestFilterTransactions
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterTransactions, _Mapping]] = ...) -> None: ...
class TransactionsStatusEntry(_message.Message):
__slots__ = ("key", "value")
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: SubscribeRequestFilterTransactions
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterTransactions, _Mapping]] = ...) -> None: ...
class BlocksEntry(_message.Message):
__slots__ = ("key", "value")
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: SubscribeRequestFilterBlocks
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterBlocks, _Mapping]] = ...) -> None: ...
class BlocksMetaEntry(_message.Message):
__slots__ = ("key", "value")
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: SubscribeRequestFilterBlocksMeta
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterBlocksMeta, _Mapping]] = ...) -> None: ...
class EntryEntry(_message.Message):
__slots__ = ("key", "value")
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: SubscribeRequestFilterEntry
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterEntry, _Mapping]] = ...) -> None: ...
ACCOUNTS_FIELD_NUMBER: _ClassVar[int]
SLOTS_FIELD_NUMBER: _ClassVar[int]
TRANSACTIONS_FIELD_NUMBER: _ClassVar[int]
TRANSACTIONS_STATUS_FIELD_NUMBER: _ClassVar[int]
BLOCKS_FIELD_NUMBER: _ClassVar[int]
BLOCKS_META_FIELD_NUMBER: _ClassVar[int]
ENTRY_FIELD_NUMBER: _ClassVar[int]
COMMITMENT_FIELD_NUMBER: _ClassVar[int]
ACCOUNTS_DATA_SLICE_FIELD_NUMBER: _ClassVar[int]
PING_FIELD_NUMBER: _ClassVar[int]
accounts: _containers.MessageMap[str, SubscribeRequestFilterAccounts]
slots: _containers.MessageMap[str, SubscribeRequestFilterSlots]
transactions: _containers.MessageMap[str, SubscribeRequestFilterTransactions]
transactions_status: _containers.MessageMap[str, SubscribeRequestFilterTransactions]
blocks: _containers.MessageMap[str, SubscribeRequestFilterBlocks]
blocks_meta: _containers.MessageMap[str, SubscribeRequestFilterBlocksMeta]
entry: _containers.MessageMap[str, SubscribeRequestFilterEntry]
commitment: CommitmentLevel
accounts_data_slice: _containers.RepeatedCompositeFieldContainer[SubscribeRequestAccountsDataSlice]
ping: SubscribeRequestPing
def __init__(self, accounts: _Optional[_Mapping[str, SubscribeRequestFilterAccounts]] = ..., slots: _Optional[_Mapping[str, SubscribeRequestFilterSlots]] = ..., transactions: _Optional[_Mapping[str, SubscribeRequestFilterTransactions]] = ..., transactions_status: _Optional[_Mapping[str, SubscribeRequestFilterTransactions]] = ..., blocks: _Optional[_Mapping[str, SubscribeRequestFilterBlocks]] = ..., blocks_meta: _Optional[_Mapping[str, SubscribeRequestFilterBlocksMeta]] = ..., entry: _Optional[_Mapping[str, SubscribeRequestFilterEntry]] = ..., commitment: _Optional[_Union[CommitmentLevel, str]] = ..., accounts_data_slice: _Optional[_Iterable[_Union[SubscribeRequestAccountsDataSlice, _Mapping]]] = ..., ping: _Optional[_Union[SubscribeRequestPing, _Mapping]] = ...) -> None: ...
class SubscribeRequestFilterAccounts(_message.Message):
__slots__ = ("account", "owner", "filters", "nonempty_txn_signature")
ACCOUNT_FIELD_NUMBER: _ClassVar[int]
OWNER_FIELD_NUMBER: _ClassVar[int]
FILTERS_FIELD_NUMBER: _ClassVar[int]
NONEMPTY_TXN_SIGNATURE_FIELD_NUMBER: _ClassVar[int]
account: _containers.RepeatedScalarFieldContainer[str]
owner: _containers.RepeatedScalarFieldContainer[str]
filters: _containers.RepeatedCompositeFieldContainer[SubscribeRequestFilterAccountsFilter]
nonempty_txn_signature: bool
def __init__(self, account: _Optional[_Iterable[str]] = ..., owner: _Optional[_Iterable[str]] = ..., filters: _Optional[_Iterable[_Union[SubscribeRequestFilterAccountsFilter, _Mapping]]] = ..., nonempty_txn_signature: bool = ...) -> None: ...
class SubscribeRequestFilterAccountsFilter(_message.Message):
__slots__ = ("memcmp", "datasize", "token_account_state", "lamports")
MEMCMP_FIELD_NUMBER: _ClassVar[int]
DATASIZE_FIELD_NUMBER: _ClassVar[int]
TOKEN_ACCOUNT_STATE_FIELD_NUMBER: _ClassVar[int]
LAMPORTS_FIELD_NUMBER: _ClassVar[int]
memcmp: SubscribeRequestFilterAccountsFilterMemcmp
datasize: int
token_account_state: bool
lamports: SubscribeRequestFilterAccountsFilterLamports
def __init__(self, memcmp: _Optional[_Union[SubscribeRequestFilterAccountsFilterMemcmp, _Mapping]] = ..., datasize: _Optional[int] = ..., token_account_state: bool = ..., lamports: _Optional[_Union[SubscribeRequestFilterAccountsFilterLamports, _Mapping]] = ...) -> None: ...
class SubscribeRequestFilterAccountsFilterMemcmp(_message.Message):
__slots__ = ("offset", "bytes", "base58", "base64")
OFFSET_FIELD_NUMBER: _ClassVar[int]
BYTES_FIELD_NUMBER: _ClassVar[int]
BASE58_FIELD_NUMBER: _ClassVar[int]
BASE64_FIELD_NUMBER: _ClassVar[int]
offset: int
bytes: bytes
base58: str
base64: str
def __init__(self, offset: _Optional[int] = ..., bytes: _Optional[bytes] = ..., base58: _Optional[str] = ..., base64: _Optional[str] = ...) -> None: ...
class SubscribeRequestFilterAccountsFilterLamports(_message.Message):
__slots__ = ("eq", "ne", "lt", "gt")
EQ_FIELD_NUMBER: _ClassVar[int]
NE_FIELD_NUMBER: _ClassVar[int]
LT_FIELD_NUMBER: _ClassVar[int]
GT_FIELD_NUMBER: _ClassVar[int]
eq: int
ne: int
lt: int
gt: int
def __init__(self, eq: _Optional[int] = ..., ne: _Optional[int] = ..., lt: _Optional[int] = ..., gt: _Optional[int] = ...) -> None: ...
class SubscribeRequestFilterSlots(_message.Message):
__slots__ = ("filter_by_commitment",)
FILTER_BY_COMMITMENT_FIELD_NUMBER: _ClassVar[int]
filter_by_commitment: bool
def __init__(self, filter_by_commitment: bool = ...) -> None: ...
class SubscribeRequestFilterTransactions(_message.Message):
__slots__ = ("vote", "failed", "signature", "account_include", "account_exclude", "account_required")
VOTE_FIELD_NUMBER: _ClassVar[int]
FAILED_FIELD_NUMBER: _ClassVar[int]
SIGNATURE_FIELD_NUMBER: _ClassVar[int]
ACCOUNT_INCLUDE_FIELD_NUMBER: _ClassVar[int]
ACCOUNT_EXCLUDE_FIELD_NUMBER: _ClassVar[int]
ACCOUNT_REQUIRED_FIELD_NUMBER: _ClassVar[int]
vote: bool
failed: bool
signature: str
account_include: _containers.RepeatedScalarFieldContainer[str]
account_exclude: _containers.RepeatedScalarFieldContainer[str]
account_required: _containers.RepeatedScalarFieldContainer[str]
def __init__(self, vote: bool = ..., failed: bool = ..., signature: _Optional[str] = ..., account_include: _Optional[_Iterable[str]] = ..., account_exclude: _Optional[_Iterable[str]] = ..., account_required: _Optional[_Iterable[str]] = ...) -> None: ...
class SubscribeRequestFilterBlocks(_message.Message):
__slots__ = ("account_include", "include_transactions", "include_accounts", "include_entries")
ACCOUNT_INCLUDE_FIELD_NUMBER: _ClassVar[int]
INCLUDE_TRANSACTIONS_FIELD_NUMBER: _ClassVar[int]
INCLUDE_ACCOUNTS_FIELD_NUMBER: _ClassVar[int]
INCLUDE_ENTRIES_FIELD_NUMBER: _ClassVar[int]
account_include: _containers.RepeatedScalarFieldContainer[str]
include_transactions: bool
include_accounts: bool
include_entries: bool
def __init__(self, account_include: _Optional[_Iterable[str]] = ..., include_transactions: bool = ..., include_accounts: bool = ..., include_entries: bool = ...) -> None: ...
class SubscribeRequestFilterBlocksMeta(_message.Message):
__slots__ = ()
def __init__(self) -> None: ...
class SubscribeRequestFilterEntry(_message.Message):
__slots__ = ()
def __init__(self) -> None: ...
class SubscribeRequestAccountsDataSlice(_message.Message):
__slots__ = ("offset", "length")
OFFSET_FIELD_NUMBER: _ClassVar[int]
LENGTH_FIELD_NUMBER: _ClassVar[int]
offset: int
length: int
def __init__(self, offset: _Optional[int] = ..., length: _Optional[int] = ...) -> None: ...
class SubscribeRequestPing(_message.Message):
__slots__ = ("id",)
ID_FIELD_NUMBER: _ClassVar[int]
id: int
def __init__(self, id: _Optional[int] = ...) -> None: ...
class SubscribeUpdate(_message.Message):
__slots__ = ("filters", "account", "slot", "transaction", "transaction_status", "block", "ping", "pong", "block_meta", "entry")
FILTERS_FIELD_NUMBER: _ClassVar[int]
ACCOUNT_FIELD_NUMBER: _ClassVar[int]
SLOT_FIELD_NUMBER: _ClassVar[int]
TRANSACTION_FIELD_NUMBER: _ClassVar[int]
TRANSACTION_STATUS_FIELD_NUMBER: _ClassVar[int]
BLOCK_FIELD_NUMBER: _ClassVar[int]
PING_FIELD_NUMBER: _ClassVar[int]
PONG_FIELD_NUMBER: _ClassVar[int]
BLOCK_META_FIELD_NUMBER: _ClassVar[int]
ENTRY_FIELD_NUMBER: _ClassVar[int]
filters: _containers.RepeatedScalarFieldContainer[str]
account: SubscribeUpdateAccount
slot: SubscribeUpdateSlot
transaction: SubscribeUpdateTransaction
transaction_status: SubscribeUpdateTransactionStatus
block: SubscribeUpdateBlock
ping: SubscribeUpdatePing
pong: SubscribeUpdatePong
block_meta: SubscribeUpdateBlockMeta
entry: SubscribeUpdateEntry
def __init__(self, filters: _Optional[_Iterable[str]] = ..., account: _Optional[_Union[SubscribeUpdateAccount, _Mapping]] = ..., slot: _Optional[_Union[SubscribeUpdateSlot, _Mapping]] = ..., transaction: _Optional[_Union[SubscribeUpdateTransaction, _Mapping]] = ..., transaction_status: _Optional[_Union[SubscribeUpdateTransactionStatus, _Mapping]] = ..., block: _Optional[_Union[SubscribeUpdateBlock, _Mapping]] = ..., ping: _Optional[_Union[SubscribeUpdatePing, _Mapping]] = ..., pong: _Optional[_Union[SubscribeUpdatePong, _Mapping]] = ..., block_meta: _Optional[_Union[SubscribeUpdateBlockMeta, _Mapping]] = ..., entry: _Optional[_Union[SubscribeUpdateEntry, _Mapping]] = ...) -> None: ...
class SubscribeUpdateAccount(_message.Message):
__slots__ = ("account", "slot", "is_startup")
ACCOUNT_FIELD_NUMBER: _ClassVar[int]
SLOT_FIELD_NUMBER: _ClassVar[int]
IS_STARTUP_FIELD_NUMBER: _ClassVar[int]
account: SubscribeUpdateAccountInfo
slot: int
is_startup: bool
def __init__(self, account: _Optional[_Union[SubscribeUpdateAccountInfo, _Mapping]] = ..., slot: _Optional[int] = ..., is_startup: bool = ...) -> None: ...
class SubscribeUpdateAccountInfo(_message.Message):
__slots__ = ("pubkey", "lamports", "owner", "executable", "rent_epoch", "data", "write_version", "txn_signature")
PUBKEY_FIELD_NUMBER: _ClassVar[int]
LAMPORTS_FIELD_NUMBER: _ClassVar[int]
OWNER_FIELD_NUMBER: _ClassVar[int]
EXECUTABLE_FIELD_NUMBER: _ClassVar[int]
RENT_EPOCH_FIELD_NUMBER: _ClassVar[int]
DATA_FIELD_NUMBER: _ClassVar[int]
WRITE_VERSION_FIELD_NUMBER: _ClassVar[int]
TXN_SIGNATURE_FIELD_NUMBER: _ClassVar[int]
pubkey: bytes
lamports: int
owner: bytes
executable: bool
rent_epoch: int
data: bytes
write_version: int
txn_signature: bytes
def __init__(self, pubkey: _Optional[bytes] = ..., lamports: _Optional[int] = ..., owner: _Optional[bytes] = ..., executable: bool = ..., rent_epoch: _Optional[int] = ..., data: _Optional[bytes] = ..., write_version: _Optional[int] = ..., txn_signature: _Optional[bytes] = ...) -> None: ...
class SubscribeUpdateSlot(_message.Message):
__slots__ = ("slot", "parent", "status", "dead_error")
SLOT_FIELD_NUMBER: _ClassVar[int]
PARENT_FIELD_NUMBER: _ClassVar[int]
STATUS_FIELD_NUMBER: _ClassVar[int]
DEAD_ERROR_FIELD_NUMBER: _ClassVar[int]
slot: int
parent: int
status: CommitmentLevel
dead_error: str
def __init__(self, slot: _Optional[int] = ..., parent: _Optional[int] = ..., status: _Optional[_Union[CommitmentLevel, str]] = ..., dead_error: _Optional[str] = ...) -> None: ...
class SubscribeUpdateTransaction(_message.Message):
__slots__ = ("transaction", "slot")
TRANSACTION_FIELD_NUMBER: _ClassVar[int]
SLOT_FIELD_NUMBER: _ClassVar[int]
transaction: SubscribeUpdateTransactionInfo
slot: int
def __init__(self, transaction: _Optional[_Union[SubscribeUpdateTransactionInfo, _Mapping]] = ..., slot: _Optional[int] = ...) -> None: ...
class SubscribeUpdateTransactionInfo(_message.Message):
__slots__ = ("signature", "is_vote", "transaction", "meta", "index")
SIGNATURE_FIELD_NUMBER: _ClassVar[int]
IS_VOTE_FIELD_NUMBER: _ClassVar[int]
TRANSACTION_FIELD_NUMBER: _ClassVar[int]
META_FIELD_NUMBER: _ClassVar[int]
INDEX_FIELD_NUMBER: _ClassVar[int]
signature: bytes
is_vote: bool
transaction: _solana_storage_pb2.Transaction
meta: _solana_storage_pb2.TransactionStatusMeta
index: int
def __init__(self, signature: _Optional[bytes] = ..., is_vote: bool = ..., transaction: _Optional[_Union[_solana_storage_pb2.Transaction, _Mapping]] = ..., meta: _Optional[_Union[_solana_storage_pb2.TransactionStatusMeta, _Mapping]] = ..., index: _Optional[int] = ...) -> None: ...
class SubscribeUpdateTransactionStatus(_message.Message):
__slots__ = ("slot", "signature", "is_vote", "index", "err")
SLOT_FIELD_NUMBER: _ClassVar[int]
SIGNATURE_FIELD_NUMBER: _ClassVar[int]
IS_VOTE_FIELD_NUMBER: _ClassVar[int]
INDEX_FIELD_NUMBER: _ClassVar[int]
ERR_FIELD_NUMBER: _ClassVar[int]
slot: int
signature: bytes
is_vote: bool
index: int
err: _solana_storage_pb2.TransactionError
def __init__(self, slot: _Optional[int] = ..., signature: _Optional[bytes] = ..., is_vote: bool = ..., index: _Optional[int] = ..., err: _Optional[_Union[_solana_storage_pb2.TransactionError, _Mapping]] = ...) -> None: ...
class SubscribeUpdateBlock(_message.Message):
__slots__ = ("slot", "blockhash", "rewards", "block_time", "block_height", "parent_slot", "parent_blockhash", "executed_transaction_count", "transactions", "updated_account_count", "accounts", "entries_count", "entries")
SLOT_FIELD_NUMBER: _ClassVar[int]
BLOCKHASH_FIELD_NUMBER: _ClassVar[int]
REWARDS_FIELD_NUMBER: _ClassVar[int]
BLOCK_TIME_FIELD_NUMBER: _ClassVar[int]
BLOCK_HEIGHT_FIELD_NUMBER: _ClassVar[int]
PARENT_SLOT_FIELD_NUMBER: _ClassVar[int]
PARENT_BLOCKHASH_FIELD_NUMBER: _ClassVar[int]
EXECUTED_TRANSACTION_COUNT_FIELD_NUMBER: _ClassVar[int]
TRANSACTIONS_FIELD_NUMBER: _ClassVar[int]
UPDATED_ACCOUNT_COUNT_FIELD_NUMBER: _ClassVar[int]
ACCOUNTS_FIELD_NUMBER: _ClassVar[int]
ENTRIES_COUNT_FIELD_NUMBER: _ClassVar[int]
ENTRIES_FIELD_NUMBER: _ClassVar[int]
slot: int
blockhash: str
rewards: _solana_storage_pb2.Rewards
block_time: _solana_storage_pb2.UnixTimestamp
block_height: _solana_storage_pb2.BlockHeight
parent_slot: int
parent_blockhash: str
executed_transaction_count: int
transactions: _containers.RepeatedCompositeFieldContainer[SubscribeUpdateTransactionInfo]
updated_account_count: int
accounts: _containers.RepeatedCompositeFieldContainer[SubscribeUpdateAccountInfo]
entries_count: int
entries: _containers.RepeatedCompositeFieldContainer[SubscribeUpdateEntry]
def __init__(self, slot: _Optional[int] = ..., blockhash: _Optional[str] = ..., rewards: _Optional[_Union[_solana_storage_pb2.Rewards, _Mapping]] = ..., block_time: _Optional[_Union[_solana_storage_pb2.UnixTimestamp, _Mapping]] = ..., block_height: _Optional[_Union[_solana_storage_pb2.BlockHeight, _Mapping]] = ..., parent_slot: _Optional[int] = ..., parent_blockhash: _Optional[str] = ..., executed_transaction_count: _Optional[int] = ..., transactions: _Optional[_Iterable[_Union[SubscribeUpdateTransactionInfo, _Mapping]]] = ..., updated_account_count: _Optional[int] = ..., accounts: _Optional[_Iterable[_Union[SubscribeUpdateAccountInfo, _Mapping]]] = ..., entries_count: _Optional[int] = ..., entries: _Optional[_Iterable[_Union[SubscribeUpdateEntry, _Mapping]]] = ...) -> None: ...
class SubscribeUpdateBlockMeta(_message.Message):
__slots__ = ("slot", "blockhash", "rewards", "block_time", "block_height", "parent_slot", "parent_blockhash", "executed_transaction_count", "entries_count")
SLOT_FIELD_NUMBER: _ClassVar[int]
BLOCKHASH_FIELD_NUMBER: _ClassVar[int]
REWARDS_FIELD_NUMBER: _ClassVar[int]
BLOCK_TIME_FIELD_NUMBER: _ClassVar[int]
BLOCK_HEIGHT_FIELD_NUMBER: _ClassVar[int]
PARENT_SLOT_FIELD_NUMBER: _ClassVar[int]
PARENT_BLOCKHASH_FIELD_NUMBER: _ClassVar[int]
EXECUTED_TRANSACTION_COUNT_FIELD_NUMBER: _ClassVar[int]
ENTRIES_COUNT_FIELD_NUMBER: _ClassVar[int]
slot: int
blockhash: str
rewards: _solana_storage_pb2.Rewards
block_time: _solana_storage_pb2.UnixTimestamp
block_height: _solana_storage_pb2.BlockHeight
parent_slot: int
parent_blockhash: str
executed_transaction_count: int
entries_count: int
def __init__(self, slot: _Optional[int] = ..., blockhash: _Optional[str] = ..., rewards: _Optional[_Union[_solana_storage_pb2.Rewards, _Mapping]] = ..., block_time: _Optional[_Union[_solana_storage_pb2.UnixTimestamp, _Mapping]] = ..., block_height: _Optional[_Union[_solana_storage_pb2.BlockHeight, _Mapping]] = ..., parent_slot: _Optional[int] = ..., parent_blockhash: _Optional[str] = ..., executed_transaction_count: _Optional[int] = ..., entries_count: _Optional[int] = ...) -> None: ...
class SubscribeUpdateEntry(_message.Message):
__slots__ = ("slot", "index", "num_hashes", "hash", "executed_transaction_count", "starting_transaction_index")
SLOT_FIELD_NUMBER: _ClassVar[int]
INDEX_FIELD_NUMBER: _ClassVar[int]
NUM_HASHES_FIELD_NUMBER: _ClassVar[int]
HASH_FIELD_NUMBER: _ClassVar[int]
EXECUTED_TRANSACTION_COUNT_FIELD_NUMBER: _ClassVar[int]
STARTING_TRANSACTION_INDEX_FIELD_NUMBER: _ClassVar[int]
slot: int
index: int
num_hashes: int
hash: bytes
executed_transaction_count: int
starting_transaction_index: int
def __init__(self, slot: _Optional[int] = ..., index: _Optional[int] = ..., num_hashes: _Optional[int] = ..., hash: _Optional[bytes] = ..., executed_transaction_count: _Optional[int] = ..., starting_transaction_index: _Optional[int] = ...) -> None: ...
class SubscribeUpdatePing(_message.Message):
__slots__ = ()
def __init__(self) -> None: ...
class SubscribeUpdatePong(_message.Message):
__slots__ = ("id",)
ID_FIELD_NUMBER: _ClassVar[int]
id: int
def __init__(self, id: _Optional[int] = ...) -> None: ...
class PingRequest(_message.Message):
__slots__ = ("count",)
COUNT_FIELD_NUMBER: _ClassVar[int]
count: int
def __init__(self, count: _Optional[int] = ...) -> None: ...
class PongResponse(_message.Message):
__slots__ = ("count",)
COUNT_FIELD_NUMBER: _ClassVar[int]
count: int
def __init__(self, count: _Optional[int] = ...) -> None: ...
class GetLatestBlockhashRequest(_message.Message):
__slots__ = ("commitment",)
COMMITMENT_FIELD_NUMBER: _ClassVar[int]
commitment: CommitmentLevel
def __init__(self, commitment: _Optional[_Union[CommitmentLevel, str]] = ...) -> None: ...
class GetLatestBlockhashResponse(_message.Message):
__slots__ = ("slot", "blockhash", "last_valid_block_height")
SLOT_FIELD_NUMBER: _ClassVar[int]
BLOCKHASH_FIELD_NUMBER: _ClassVar[int]
LAST_VALID_BLOCK_HEIGHT_FIELD_NUMBER: _ClassVar[int]
slot: int
blockhash: str
last_valid_block_height: int
def __init__(self, slot: _Optional[int] = ..., blockhash: _Optional[str] = ..., last_valid_block_height: _Optional[int] = ...) -> None: ...
class GetBlockHeightRequest(_message.Message):
__slots__ = ("commitment",)
COMMITMENT_FIELD_NUMBER: _ClassVar[int]
commitment: CommitmentLevel
def __init__(self, commitment: _Optional[_Union[CommitmentLevel, str]] = ...) -> None: ...
class GetBlockHeightResponse(_message.Message):
__slots__ = ("block_height",)
BLOCK_HEIGHT_FIELD_NUMBER: _ClassVar[int]
block_height: int
def __init__(self, block_height: _Optional[int] = ...) -> None: ...
class GetSlotRequest(_message.Message):
__slots__ = ("commitment",)
COMMITMENT_FIELD_NUMBER: _ClassVar[int]
commitment: CommitmentLevel
def __init__(self, commitment: _Optional[_Union[CommitmentLevel, str]] = ...) -> None: ...
class GetSlotResponse(_message.Message):
__slots__ = ("slot",)
SLOT_FIELD_NUMBER: _ClassVar[int]
slot: int
def __init__(self, slot: _Optional[int] = ...) -> None: ...
class GetVersionRequest(_message.Message):
__slots__ = ()
def __init__(self) -> None: ...
class GetVersionResponse(_message.Message):
__slots__ = ("version",)
VERSION_FIELD_NUMBER: _ClassVar[int]
version: str
def __init__(self, version: _Optional[str] = ...) -> None: ...
class IsBlockhashValidRequest(_message.Message):
__slots__ = ("blockhash", "commitment")
BLOCKHASH_FIELD_NUMBER: _ClassVar[int]
COMMITMENT_FIELD_NUMBER: _ClassVar[int]
blockhash: str
commitment: CommitmentLevel
def __init__(self, blockhash: _Optional[str] = ..., commitment: _Optional[_Union[CommitmentLevel, str]] = ...) -> None: ...
class IsBlockhashValidResponse(_message.Message):
__slots__ = ("slot", "valid")
SLOT_FIELD_NUMBER: _ClassVar[int]
VALID_FIELD_NUMBER: _ClassVar[int]
slot: int
valid: bool
def __init__(self, slot: _Optional[int] = ..., valid: bool = ...) -> None: ...
@@ -0,0 +1,355 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
import generated.geyser_pb2 as geyser__pb2
GRPC_GENERATED_VERSION = '1.71.0'
GRPC_VERSION = grpc.__version__
_version_not_supported = False
try:
from grpc._utilities import first_version_is_lower
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
except ImportError:
_version_not_supported = True
if _version_not_supported:
raise RuntimeError(
f'The grpc package installed is at version {GRPC_VERSION},'
+ ' but the generated code in geyser_pb2_grpc.py depends on'
+ f' 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}.'
)
class GeyserStub:
"""Missing associated documentation comment in .proto file."""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.Subscribe = channel.stream_stream(
'/geyser.Geyser/Subscribe',
request_serializer=geyser__pb2.SubscribeRequest.SerializeToString,
response_deserializer=geyser__pb2.SubscribeUpdate.FromString,
_registered_method=True)
self.Ping = channel.unary_unary(
'/geyser.Geyser/Ping',
request_serializer=geyser__pb2.PingRequest.SerializeToString,
response_deserializer=geyser__pb2.PongResponse.FromString,
_registered_method=True)
self.GetLatestBlockhash = channel.unary_unary(
'/geyser.Geyser/GetLatestBlockhash',
request_serializer=geyser__pb2.GetLatestBlockhashRequest.SerializeToString,
response_deserializer=geyser__pb2.GetLatestBlockhashResponse.FromString,
_registered_method=True)
self.GetBlockHeight = channel.unary_unary(
'/geyser.Geyser/GetBlockHeight',
request_serializer=geyser__pb2.GetBlockHeightRequest.SerializeToString,
response_deserializer=geyser__pb2.GetBlockHeightResponse.FromString,
_registered_method=True)
self.GetSlot = channel.unary_unary(
'/geyser.Geyser/GetSlot',
request_serializer=geyser__pb2.GetSlotRequest.SerializeToString,
response_deserializer=geyser__pb2.GetSlotResponse.FromString,
_registered_method=True)
self.IsBlockhashValid = channel.unary_unary(
'/geyser.Geyser/IsBlockhashValid',
request_serializer=geyser__pb2.IsBlockhashValidRequest.SerializeToString,
response_deserializer=geyser__pb2.IsBlockhashValidResponse.FromString,
_registered_method=True)
self.GetVersion = channel.unary_unary(
'/geyser.Geyser/GetVersion',
request_serializer=geyser__pb2.GetVersionRequest.SerializeToString,
response_deserializer=geyser__pb2.GetVersionResponse.FromString,
_registered_method=True)
class GeyserServicer:
"""Missing associated documentation comment in .proto file."""
def Subscribe(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 Ping(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 GetLatestBlockhash(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 GetBlockHeight(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 GetSlot(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 IsBlockhashValid(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 GetVersion(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 add_GeyserServicer_to_server(servicer, server):
rpc_method_handlers = {
'Subscribe': grpc.stream_stream_rpc_method_handler(
servicer.Subscribe,
request_deserializer=geyser__pb2.SubscribeRequest.FromString,
response_serializer=geyser__pb2.SubscribeUpdate.SerializeToString,
),
'Ping': grpc.unary_unary_rpc_method_handler(
servicer.Ping,
request_deserializer=geyser__pb2.PingRequest.FromString,
response_serializer=geyser__pb2.PongResponse.SerializeToString,
),
'GetLatestBlockhash': grpc.unary_unary_rpc_method_handler(
servicer.GetLatestBlockhash,
request_deserializer=geyser__pb2.GetLatestBlockhashRequest.FromString,
response_serializer=geyser__pb2.GetLatestBlockhashResponse.SerializeToString,
),
'GetBlockHeight': grpc.unary_unary_rpc_method_handler(
servicer.GetBlockHeight,
request_deserializer=geyser__pb2.GetBlockHeightRequest.FromString,
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(
'geyser.Geyser', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
server.add_registered_method_handlers('geyser.Geyser', rpc_method_handlers)
# This class is part of an EXPERIMENTAL API.
class Geyser:
"""Missing associated documentation comment in .proto file."""
@staticmethod
def Subscribe(request_iterator,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.stream_stream(
request_iterator,
target,
'/geyser.Geyser/Subscribe',
geyser__pb2.SubscribeRequest.SerializeToString,
geyser__pb2.SubscribeUpdate.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.PongResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def GetLatestBlockhash(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/GetLatestBlockhash',
geyser__pb2.GetLatestBlockhashRequest.SerializeToString,
geyser__pb2.GetLatestBlockhashResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def GetBlockHeight(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/GetBlockHeight',
geyser__pb2.GetBlockHeightRequest.SerializeToString,
geyser__pb2.GetBlockHeightResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def GetSlot(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/GetSlot',
geyser__pb2.GetSlotRequest.SerializeToString,
geyser__pb2.GetSlotResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def IsBlockhashValid(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/IsBlockhashValid',
geyser__pb2.IsBlockhashValidRequest.SerializeToString,
geyser__pb2.IsBlockhashValidResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def GetVersion(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/GetVersion',
geyser__pb2.GetVersionRequest.SerializeToString,
geyser__pb2.GetVersionResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
File diff suppressed because one or more lines are too long
@@ -0,0 +1,238 @@
from google.protobuf.internal import containers as _containers
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
DESCRIPTOR: _descriptor.FileDescriptor
class RewardType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
Unspecified: _ClassVar[RewardType]
Fee: _ClassVar[RewardType]
Rent: _ClassVar[RewardType]
Staking: _ClassVar[RewardType]
Voting: _ClassVar[RewardType]
Unspecified: RewardType
Fee: RewardType
Rent: RewardType
Staking: RewardType
Voting: RewardType
class ConfirmedBlock(_message.Message):
__slots__ = ("previous_blockhash", "blockhash", "parent_slot", "transactions", "rewards", "block_time", "block_height", "num_partitions")
PREVIOUS_BLOCKHASH_FIELD_NUMBER: _ClassVar[int]
BLOCKHASH_FIELD_NUMBER: _ClassVar[int]
PARENT_SLOT_FIELD_NUMBER: _ClassVar[int]
TRANSACTIONS_FIELD_NUMBER: _ClassVar[int]
REWARDS_FIELD_NUMBER: _ClassVar[int]
BLOCK_TIME_FIELD_NUMBER: _ClassVar[int]
BLOCK_HEIGHT_FIELD_NUMBER: _ClassVar[int]
NUM_PARTITIONS_FIELD_NUMBER: _ClassVar[int]
previous_blockhash: str
blockhash: str
parent_slot: int
transactions: _containers.RepeatedCompositeFieldContainer[ConfirmedTransaction]
rewards: _containers.RepeatedCompositeFieldContainer[Reward]
block_time: UnixTimestamp
block_height: BlockHeight
num_partitions: NumPartitions
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: ...
class ConfirmedTransaction(_message.Message):
__slots__ = ("transaction", "meta")
TRANSACTION_FIELD_NUMBER: _ClassVar[int]
META_FIELD_NUMBER: _ClassVar[int]
transaction: Transaction
meta: TransactionStatusMeta
def __init__(self, transaction: _Optional[_Union[Transaction, _Mapping]] = ..., meta: _Optional[_Union[TransactionStatusMeta, _Mapping]] = ...) -> None: ...
class Transaction(_message.Message):
__slots__ = ("signatures", "message")
SIGNATURES_FIELD_NUMBER: _ClassVar[int]
MESSAGE_FIELD_NUMBER: _ClassVar[int]
signatures: _containers.RepeatedScalarFieldContainer[bytes]
message: Message
def __init__(self, signatures: _Optional[_Iterable[bytes]] = ..., message: _Optional[_Union[Message, _Mapping]] = ...) -> None: ...
class Message(_message.Message):
__slots__ = ("header", "account_keys", "recent_blockhash", "instructions", "versioned", "address_table_lookups")
HEADER_FIELD_NUMBER: _ClassVar[int]
ACCOUNT_KEYS_FIELD_NUMBER: _ClassVar[int]
RECENT_BLOCKHASH_FIELD_NUMBER: _ClassVar[int]
INSTRUCTIONS_FIELD_NUMBER: _ClassVar[int]
VERSIONED_FIELD_NUMBER: _ClassVar[int]
ADDRESS_TABLE_LOOKUPS_FIELD_NUMBER: _ClassVar[int]
header: MessageHeader
account_keys: _containers.RepeatedScalarFieldContainer[bytes]
recent_blockhash: bytes
instructions: _containers.RepeatedCompositeFieldContainer[CompiledInstruction]
versioned: bool
address_table_lookups: _containers.RepeatedCompositeFieldContainer[MessageAddressTableLookup]
def __init__(self, header: _Optional[_Union[MessageHeader, _Mapping]] = ..., account_keys: _Optional[_Iterable[bytes]] = ..., recent_blockhash: _Optional[bytes] = ..., instructions: _Optional[_Iterable[_Union[CompiledInstruction, _Mapping]]] = ..., versioned: bool = ..., address_table_lookups: _Optional[_Iterable[_Union[MessageAddressTableLookup, _Mapping]]] = ...) -> None: ...
class MessageHeader(_message.Message):
__slots__ = ("num_required_signatures", "num_readonly_signed_accounts", "num_readonly_unsigned_accounts")
NUM_REQUIRED_SIGNATURES_FIELD_NUMBER: _ClassVar[int]
NUM_READONLY_SIGNED_ACCOUNTS_FIELD_NUMBER: _ClassVar[int]
NUM_READONLY_UNSIGNED_ACCOUNTS_FIELD_NUMBER: _ClassVar[int]
num_required_signatures: int
num_readonly_signed_accounts: int
num_readonly_unsigned_accounts: int
def __init__(self, num_required_signatures: _Optional[int] = ..., num_readonly_signed_accounts: _Optional[int] = ..., num_readonly_unsigned_accounts: _Optional[int] = ...) -> None: ...
class MessageAddressTableLookup(_message.Message):
__slots__ = ("account_key", "writable_indexes", "readonly_indexes")
ACCOUNT_KEY_FIELD_NUMBER: _ClassVar[int]
WRITABLE_INDEXES_FIELD_NUMBER: _ClassVar[int]
READONLY_INDEXES_FIELD_NUMBER: _ClassVar[int]
account_key: bytes
writable_indexes: bytes
readonly_indexes: bytes
def __init__(self, account_key: _Optional[bytes] = ..., writable_indexes: _Optional[bytes] = ..., readonly_indexes: _Optional[bytes] = ...) -> None: ...
class TransactionStatusMeta(_message.Message):
__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")
ERR_FIELD_NUMBER: _ClassVar[int]
FEE_FIELD_NUMBER: _ClassVar[int]
PRE_BALANCES_FIELD_NUMBER: _ClassVar[int]
POST_BALANCES_FIELD_NUMBER: _ClassVar[int]
INNER_INSTRUCTIONS_FIELD_NUMBER: _ClassVar[int]
INNER_INSTRUCTIONS_NONE_FIELD_NUMBER: _ClassVar[int]
LOG_MESSAGES_FIELD_NUMBER: _ClassVar[int]
LOG_MESSAGES_NONE_FIELD_NUMBER: _ClassVar[int]
PRE_TOKEN_BALANCES_FIELD_NUMBER: _ClassVar[int]
POST_TOKEN_BALANCES_FIELD_NUMBER: _ClassVar[int]
REWARDS_FIELD_NUMBER: _ClassVar[int]
LOADED_WRITABLE_ADDRESSES_FIELD_NUMBER: _ClassVar[int]
LOADED_READONLY_ADDRESSES_FIELD_NUMBER: _ClassVar[int]
RETURN_DATA_FIELD_NUMBER: _ClassVar[int]
RETURN_DATA_NONE_FIELD_NUMBER: _ClassVar[int]
COMPUTE_UNITS_CONSUMED_FIELD_NUMBER: _ClassVar[int]
err: TransactionError
fee: int
pre_balances: _containers.RepeatedScalarFieldContainer[int]
post_balances: _containers.RepeatedScalarFieldContainer[int]
inner_instructions: _containers.RepeatedCompositeFieldContainer[InnerInstructions]
inner_instructions_none: bool
log_messages: _containers.RepeatedScalarFieldContainer[str]
log_messages_none: bool
pre_token_balances: _containers.RepeatedCompositeFieldContainer[TokenBalance]
post_token_balances: _containers.RepeatedCompositeFieldContainer[TokenBalance]
rewards: _containers.RepeatedCompositeFieldContainer[Reward]
loaded_writable_addresses: _containers.RepeatedScalarFieldContainer[bytes]
loaded_readonly_addresses: _containers.RepeatedScalarFieldContainer[bytes]
return_data: ReturnData
return_data_none: bool
compute_units_consumed: int
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: 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):
__slots__ = ("err",)
ERR_FIELD_NUMBER: _ClassVar[int]
err: bytes
def __init__(self, err: _Optional[bytes] = ...) -> None: ...
class InnerInstructions(_message.Message):
__slots__ = ("index", "instructions")
INDEX_FIELD_NUMBER: _ClassVar[int]
INSTRUCTIONS_FIELD_NUMBER: _ClassVar[int]
index: int
instructions: _containers.RepeatedCompositeFieldContainer[InnerInstruction]
def __init__(self, index: _Optional[int] = ..., instructions: _Optional[_Iterable[_Union[InnerInstruction, _Mapping]]] = ...) -> None: ...
class InnerInstruction(_message.Message):
__slots__ = ("program_id_index", "accounts", "data", "stack_height")
PROGRAM_ID_INDEX_FIELD_NUMBER: _ClassVar[int]
ACCOUNTS_FIELD_NUMBER: _ClassVar[int]
DATA_FIELD_NUMBER: _ClassVar[int]
STACK_HEIGHT_FIELD_NUMBER: _ClassVar[int]
program_id_index: int
accounts: bytes
data: bytes
stack_height: int
def __init__(self, program_id_index: _Optional[int] = ..., accounts: _Optional[bytes] = ..., data: _Optional[bytes] = ..., stack_height: _Optional[int] = ...) -> None: ...
class CompiledInstruction(_message.Message):
__slots__ = ("program_id_index", "accounts", "data")
PROGRAM_ID_INDEX_FIELD_NUMBER: _ClassVar[int]
ACCOUNTS_FIELD_NUMBER: _ClassVar[int]
DATA_FIELD_NUMBER: _ClassVar[int]
program_id_index: int
accounts: bytes
data: bytes
def __init__(self, program_id_index: _Optional[int] = ..., accounts: _Optional[bytes] = ..., data: _Optional[bytes] = ...) -> None: ...
class TokenBalance(_message.Message):
__slots__ = ("account_index", "mint", "ui_token_amount", "owner", "program_id")
ACCOUNT_INDEX_FIELD_NUMBER: _ClassVar[int]
MINT_FIELD_NUMBER: _ClassVar[int]
UI_TOKEN_AMOUNT_FIELD_NUMBER: _ClassVar[int]
OWNER_FIELD_NUMBER: _ClassVar[int]
PROGRAM_ID_FIELD_NUMBER: _ClassVar[int]
account_index: int
mint: str
ui_token_amount: UiTokenAmount
owner: str
program_id: str
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: ...
class UiTokenAmount(_message.Message):
__slots__ = ("ui_amount", "decimals", "amount", "ui_amount_string")
UI_AMOUNT_FIELD_NUMBER: _ClassVar[int]
DECIMALS_FIELD_NUMBER: _ClassVar[int]
AMOUNT_FIELD_NUMBER: _ClassVar[int]
UI_AMOUNT_STRING_FIELD_NUMBER: _ClassVar[int]
ui_amount: float
decimals: int
amount: str
ui_amount_string: str
def __init__(self, ui_amount: _Optional[float] = ..., decimals: _Optional[int] = ..., amount: _Optional[str] = ..., ui_amount_string: _Optional[str] = ...) -> None: ...
class ReturnData(_message.Message):
__slots__ = ("program_id", "data")
PROGRAM_ID_FIELD_NUMBER: _ClassVar[int]
DATA_FIELD_NUMBER: _ClassVar[int]
program_id: bytes
data: bytes
def __init__(self, program_id: _Optional[bytes] = ..., data: _Optional[bytes] = ...) -> None: ...
class Reward(_message.Message):
__slots__ = ("pubkey", "lamports", "post_balance", "reward_type", "commission")
PUBKEY_FIELD_NUMBER: _ClassVar[int]
LAMPORTS_FIELD_NUMBER: _ClassVar[int]
POST_BALANCE_FIELD_NUMBER: _ClassVar[int]
REWARD_TYPE_FIELD_NUMBER: _ClassVar[int]
COMMISSION_FIELD_NUMBER: _ClassVar[int]
pubkey: str
lamports: int
post_balance: int
reward_type: RewardType
commission: str
def __init__(self, pubkey: _Optional[str] = ..., lamports: _Optional[int] = ..., post_balance: _Optional[int] = ..., reward_type: _Optional[_Union[RewardType, str]] = ..., commission: _Optional[str] = ...) -> None: ...
class Rewards(_message.Message):
__slots__ = ("rewards", "num_partitions")
REWARDS_FIELD_NUMBER: _ClassVar[int]
NUM_PARTITIONS_FIELD_NUMBER: _ClassVar[int]
rewards: _containers.RepeatedCompositeFieldContainer[Reward]
num_partitions: NumPartitions
def __init__(self, rewards: _Optional[_Iterable[_Union[Reward, _Mapping]]] = ..., num_partitions: _Optional[_Union[NumPartitions, _Mapping]] = ...) -> None: ...
class UnixTimestamp(_message.Message):
__slots__ = ("timestamp",)
TIMESTAMP_FIELD_NUMBER: _ClassVar[int]
timestamp: int
def __init__(self, timestamp: _Optional[int] = ...) -> None: ...
class BlockHeight(_message.Message):
__slots__ = ("block_height",)
BLOCK_HEIGHT_FIELD_NUMBER: _ClassVar[int]
block_height: int
def __init__(self, block_height: _Optional[int] = ...) -> None: ...
class NumPartitions(_message.Message):
__slots__ = ("num_partitions",)
NUM_PARTITIONS_FIELD_NUMBER: _ClassVar[int]
num_partitions: int
def __init__(self, num_partitions: _Optional[int] = ...) -> None: ...
@@ -0,0 +1,24 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
import warnings
GRPC_GENERATED_VERSION = '1.71.0'
GRPC_VERSION = grpc.__version__
_version_not_supported = False
try:
from grpc._utilities import first_version_is_lower
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
except ImportError:
_version_not_supported = True
if _version_not_supported:
raise RuntimeError(
f'The grpc package installed is at version {GRPC_VERSION},'
+ f' but the generated code in solana_storage_pb2_grpc.py depends on'
+ f' 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}.'
)
@@ -0,0 +1,161 @@
"""
Listens to Solana blocks for Pump.fun 'create' instructions via WebSocket.
Decodes transaction data to extract mint, bonding curve, and user details.
It is usually slower than other listeners.
"""
import asyncio
import base64
import json
import os
import struct
import websockets
from dotenv import load_dotenv
from solders.pubkey import Pubkey
from solders.transaction import VersionedTransaction
load_dotenv()
WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
def load_idl(file_path):
with open(file_path) as f:
return json.load(f)
def decode_create_instruction(ix_data, ix_def, accounts):
args = {}
offset = 8 # Skip 8-byte discriminator
for arg in ix_def["args"]:
if arg["type"] == "string":
length = struct.unpack_from("<I", ix_data, offset)[0]
offset += 4
value = ix_data[offset : offset + length].decode("utf-8")
offset += length
elif arg["type"] == "publicKey":
value = base64.b64encode(ix_data[offset : offset + 32]).decode("utf-8")
offset += 32
else:
raise ValueError(f"Unsupported type: {arg['type']}")
args[arg["name"]] = value
# Add accounts
args["mint"] = str(accounts[0])
args["bondingCurve"] = str(accounts[2])
args["associatedBondingCurve"] = str(accounts[3])
args["user"] = str(accounts[7])
return args
# Here and later all the discriminators are precalculated. See learning-examples/discriminator.py
async def listen_and_decode_create():
idl = load_idl("idl/pump_fun_idl.json")
create_discriminator = 8576854823835016728
async with websockets.connect(WSS_ENDPOINT) as websocket:
subscription_message = json.dumps(
{
"jsonrpc": "2.0",
"id": 1,
"method": "blockSubscribe",
"params": [
{"mentionsAccountOrProgram": str(PUMP_PROGRAM_ID)},
{
"commitment": "confirmed",
"encoding": "base64",
"showRewards": False,
"transactionDetails": "full",
"maxSupportedTransactionVersion": 0,
},
],
}
)
await websocket.send(subscription_message)
print(f"Subscribed to blocks mentioning program: {PUMP_PROGRAM_ID}")
while True:
try:
response = await websocket.recv()
data = json.loads(response)
if "method" in data and data["method"] == "blockNotification":
if "params" in data and "result" in data["params"]:
block_data = data["params"]["result"]
if "value" in block_data and "block" in block_data["value"]:
block = block_data["value"]["block"]
if "transactions" in block:
for tx in block["transactions"]:
if isinstance(tx, dict) and "transaction" in tx:
tx_data_decoded = base64.b64decode(
tx["transaction"][0]
)
transaction = VersionedTransaction.from_bytes(
tx_data_decoded
)
for ix in transaction.message.instructions:
if str(
transaction.message.account_keys[
ix.program_id_index
]
) == str(PUMP_PROGRAM_ID):
ix_data = bytes(ix.data)
discriminator = struct.unpack(
"<Q", ix_data[:8]
)[0]
if (
discriminator
== create_discriminator
):
create_ix = next(
instr
for instr in idl["instructions"]
if instr["name"] == "create"
)
account_keys = [
str(
transaction.message.account_keys[
index
]
)
for index in ix.accounts
]
decoded_args = (
decode_create_instruction(
ix_data,
create_ix,
account_keys,
)
)
print(
json.dumps(
decoded_args, indent=2
)
)
print("--------------------")
elif "result" in data:
print("Subscription confirmed")
else:
print(
f"Received unexpected message type: {data.get('method', 'Unknown')}"
)
except Exception as e:
print(f"An error occurred: {e!s}")
print(f"Error details: {type(e).__name__}")
import traceback
traceback.print_exc()
print("WebSocket connection closed.")
if __name__ == "__main__":
asyncio.run(listen_and_decode_create())
@@ -0,0 +1,128 @@
"""
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).
Requires a Geyser API token for access.
It is proven to be the fastest listener.
"""
import asyncio
import os
import struct
import base58
import grpc
from dotenv import load_dotenv
from generated import geyser_pb2, geyser_pb2_grpc
from solders.pubkey import Pubkey
load_dotenv()
GEYSER_ENDPOINT = os.getenv("GEYSER_ENDPOINT")
GEYSER_API_TOKEN = os.getenv("GEYSER_API_TOKEN")
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
PUMP_CREATE_PREFIX = struct.pack("<Q", 8576854823835016728)
async def create_geyser_connection():
"""Establish a secure connection to the Geyser endpoint."""
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():
"""Create a subscription request for Pump.fun transactions."""
request = geyser_pb2.SubscribeRequest()
request.transactions["pump_filter"].account_include.append(str(PUMP_PROGRAM_ID))
request.transactions["pump_filter"].failed = False
request.commitment = geyser_pb2.CommitmentLevel.PROCESSED
return request
def decode_create_instruction(ix_data: bytes, keys, accounts) -> dict:
"""Decode a create instruction from transaction data."""
# Skip past the 8-byte discriminator prefix
offset = 8
# Extract account keys in base58 format
def get_account_key(index):
if index >= len(accounts):
return "N/A"
account_index = accounts[index]
return base58.b58encode(keys[account_index]).decode()
# Read string fields (prefixed with length)
def read_string():
nonlocal offset
# Get string length (4-byte uint)
length = struct.unpack_from("<I", ix_data, offset)[0]
offset += 4
# Extract and decode the string
value = ix_data[offset:offset + length].decode()
offset += length
return value
name = read_string()
symbol = read_string()
uri = read_string()
token_info = {
"name": name,
"symbol": symbol,
"uri": uri,
"mint": get_account_key(0),
"metadata": get_account_key(1),
"bonding_curve": get_account_key(2),
"associated_bonding_curve": get_account_key(3),
"token_program": get_account_key(4),
"system_program": get_account_key(5),
"rent": get_account_key(6),
"user": get_account_key(7),
}
return token_info
def print_token_info(info, signature):
"""Print formatted token information."""
print("\n🎯 New Pump.fun token detected!")
print(f"Name: {info['name']} | Symbol: {info['symbol']}")
print(f"Mint: {info['mint']}")
print(f"Bonding curve: {info['bonding_curve']}")
print(f"Associated bonding curve: {info['associated_bonding_curve']}")
print(f"Signature: {signature}")
async def monitor_pump():
"""Monitor Solana blockchain for new Pump.fun token creations."""
stub = await create_geyser_connection()
request = create_subscription_request()
async for update in stub.Subscribe(iter([request])):
# Skip non-transaction updates
if not update.HasField("transaction"):
continue
tx = update.transaction.transaction.transaction
msg = getattr(tx, "message", None)
if msg is None:
continue
# Check each instruction in the transaction
for ix in msg.instructions:
if not ix.data.startswith(PUMP_CREATE_PREFIX):
continue
info = decode_create_instruction(ix.data, msg.account_keys, ix.accounts)
signature = base58.b58encode(bytes(update.transaction.transaction.signature)).decode()
print_token_info(info, signature)
if __name__ == "__main__":
asyncio.run(monitor_pump())
@@ -0,0 +1,165 @@
"""
Listens for new Pump.fun token creations via Solana WebSocket.
Monitors logs for 'Create' instructions, decodes and prints token details (name, symbol, mint, etc.).
Additionally, calculates an associated bonding curve address for each token.
It is usually faster than blockSubscribe, but slower than Geyser.
"""
import asyncio
import base64
import json
import os
import struct
import base58
import websockets
from dotenv import load_dotenv
from solders.pubkey import Pubkey
load_dotenv()
WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
TOKEN_PROGRAM_ID = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
ASSOCIATED_TOKEN_PROGRAM_ID = Pubkey.from_string("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL")
def find_associated_bonding_curve(mint: Pubkey, bonding_curve: Pubkey) -> Pubkey:
"""
Find the associated bonding curve for a given mint and bonding curve.
This uses the standard ATA derivation.
"""
derived_address, _ = Pubkey.find_program_address(
[
bytes(bonding_curve),
bytes(TOKEN_PROGRAM_ID),
bytes(mint),
],
ASSOCIATED_TOKEN_PROGRAM_ID,
)
return derived_address
def parse_create_instruction(data):
if len(data) < 8:
return None
offset = 8
parsed_data = {}
# Parse fields based on CreateEvent structure
fields = [
("name", "string"),
("symbol", "string"),
("uri", "string"),
("mint", "publicKey"),
("bondingCurve", "publicKey"),
("user", "publicKey"),
]
try:
for field_name, field_type in fields:
if field_type == "string":
length = struct.unpack("<I", data[offset : offset + 4])[0]
offset += 4
value = data[offset : offset + length].decode("utf-8")
offset += length
elif field_type == "publicKey":
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
offset += 32
parsed_data[field_name] = value
return parsed_data
except:
return None
async def listen_for_new_tokens():
while True:
try:
async with websockets.connect(WSS_ENDPOINT) as websocket:
subscription_message = json.dumps(
{
"jsonrpc": "2.0",
"id": 1,
"method": "logsSubscribe",
"params": [
{"mentions": [str(PUMP_PROGRAM_ID)]},
{"commitment": "processed"},
],
}
)
await websocket.send(subscription_message)
print(
f"Listening for new token creations from program: {PUMP_PROGRAM_ID}"
)
# Wait for subscription confirmation
response = await websocket.recv()
print(f"Subscription response: {response}")
while True:
try:
response = await websocket.recv()
data = json.loads(response)
if "method" in data and data["method"] == "logsNotification":
log_data = data["params"]["result"]["value"]
logs = log_data.get("logs", [])
if any(
"Program log: Instruction: Create" in log
for log in logs
):
for log in logs:
if "Program data:" in log:
try:
encoded_data = log.split(": ")[1]
decoded_data = base64.b64decode(
encoded_data
)
parsed_data = parse_create_instruction(
decoded_data
)
if parsed_data and "name" in parsed_data:
print(
"Signature:",
log_data.get("signature"),
)
for key, value in parsed_data.items():
print(f"{key}: {value}")
# Calculate associated bonding curve
mint = Pubkey.from_string(
parsed_data["mint"]
)
bonding_curve = Pubkey.from_string(
parsed_data["bondingCurve"]
)
associated_curve = (
find_associated_bonding_curve(
mint, bonding_curve
)
)
print(
f"Associated Bonding Curve: {associated_curve}"
)
print(
"##########################################################################################"
)
except Exception as e:
print(f"Failed to decode: {log}")
print(f"Error: {e!s}")
except Exception as e:
print(f"An error occurred while processing message: {e}")
break
except Exception as e:
print(f"Connection error: {e}")
print("Reconnecting in 5 seconds...")
await asyncio.sleep(5)
if __name__ == "__main__":
asyncio.run(listen_for_new_tokens())
@@ -0,0 +1,131 @@
"""
Listens for new Pump.fun token creations via Solana WebSocket.
Monitors logs for 'Create' instructions, decodes and prints token details (name, symbol, mint, etc.).
It is usually faster than blockSubscribe, but slower than Geyser.
"""
import asyncio
import base64
import json
import os
import struct
import base58
import websockets
from dotenv import load_dotenv
from solders.pubkey import Pubkey
load_dotenv()
WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
def parse_create_instruction(data):
if len(data) < 8:
return None
offset = 8
parsed_data = {}
# Parse fields based on CreateEvent structure
fields = [
("name", "string"),
("symbol", "string"),
("uri", "string"),
("mint", "publicKey"),
("bondingCurve", "publicKey"),
("user", "publicKey"),
]
try:
for field_name, field_type in fields:
if field_type == "string":
length = struct.unpack("<I", data[offset : offset + 4])[0]
offset += 4
value = data[offset : offset + length].decode("utf-8")
offset += length
elif field_type == "publicKey":
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
offset += 32
parsed_data[field_name] = value
return parsed_data
except:
return None
async def listen_for_new_tokens():
while True:
try:
async with websockets.connect(WSS_ENDPOINT) as websocket:
subscription_message = json.dumps(
{
"jsonrpc": "2.0",
"id": 1,
"method": "logsSubscribe",
"params": [
{"mentions": [str(PUMP_PROGRAM_ID)]},
{"commitment": "processed"},
],
}
)
await websocket.send(subscription_message)
print(
f"Listening for new token creations from program: {PUMP_PROGRAM_ID}"
)
# Wait for subscription confirmation
response = await websocket.recv()
print(f"Subscription response: {response}")
while True:
try:
response = await websocket.recv()
data = json.loads(response)
if "method" in data and data["method"] == "logsNotification":
log_data = data["params"]["result"]["value"]
logs = log_data.get("logs", [])
if any(
"Program log: Instruction: Create" in log
for log in logs
):
for log in logs:
if "Program data:" in log:
try:
encoded_data = log.split(": ")[1]
decoded_data = base64.b64decode(
encoded_data
)
parsed_data = parse_create_instruction(
decoded_data
)
if parsed_data and "name" in parsed_data:
print(
"Signature:",
log_data.get("signature"),
)
for key, value in parsed_data.items():
print(f"{key}: {value}")
print(
"##########################################################################################"
)
except Exception as e:
print(f"Failed to decode: {log}")
print(f"Error: {e!s}")
except Exception as e:
print(f"An error occurred while processing message: {e}")
break
except Exception as e:
print(f"Connection error: {e}")
print("Reconnecting in 5 seconds...")
await asyncio.sleep(5)
if __name__ == "__main__":
asyncio.run(listen_for_new_tokens())
@@ -1,24 +1,29 @@
import websockets
"""
Listens for new Pump.fun token creations via PumpPortal WebSocket.
"""
import asyncio
import json
from datetime import datetime
import websockets
# PumpPortal WebSocket URL
WS_URL = "wss://pumpportal.fun/api/data"
def format_sol(value):
return f"{value:.6f} SOL"
def format_timestamp(timestamp):
return datetime.fromtimestamp(timestamp / 1000).strftime('%Y-%m-%d %H:%M:%S')
return datetime.fromtimestamp(timestamp / 1000).strftime("%Y-%m-%d %H:%M:%S")
async def listen_for_new_tokens():
async with websockets.connect(WS_URL) as websocket:
# Subscribe to new token events
await websocket.send(json.dumps({
"method": "subscribeNewToken",
"params": []
}))
await websocket.send(json.dumps({"method": "subscribeNewToken", "params": []}))
print("Listening for new token creations...")
@@ -27,23 +32,31 @@ async def listen_for_new_tokens():
message = await websocket.recv()
data = json.loads(message)
if 'method' in data and data['method'] == 'newToken':
token_info = data.get('params', [{}])[0]
elif 'signature' in data and 'mint' in data:
if "method" in data and data["method"] == "newToken":
token_info = data.get("params", [{}])[0]
elif "signature" in data and "mint" in data:
token_info = data
else:
continue
print("\n" + "=" * 50)
print(f"New token created: {token_info.get('name')} ({token_info.get('symbol')})")
print(
f"New token created: {token_info.get('name')} ({token_info.get('symbol')})"
)
print("=" * 50)
print(f"Address: {token_info.get('mint')}")
print(f"Creator: {token_info.get('traderPublicKey')}")
print(f"Initial Buy: {format_sol(token_info.get('initialBuy', 0))}")
print(f"Market Cap: {format_sol(token_info.get('marketCapSol', 0))}")
print(
f"Market Cap: {format_sol(token_info.get('marketCapSol', 0))}"
)
print(f"Bonding Curve: {token_info.get('bondingCurveKey')}")
print(f"Virtual SOL: {format_sol(token_info.get('vSolInBondingCurve', 0))}")
print(f"Virtual Tokens: {token_info.get('vTokensInBondingCurve', 0):,.0f}")
print(
f"Virtual SOL: {format_sol(token_info.get('vSolInBondingCurve', 0))}"
)
print(
f"Virtual Tokens: {token_info.get('vTokensInBondingCurve', 0):,.0f}"
)
print(f"Metadata URI: {token_info.get('uri')}")
print(f"Signature: {token_info.get('signature')}")
print("=" * 50)
@@ -55,6 +68,7 @@ async def listen_for_new_tokens():
except Exception as e:
print(f"\nAn error occurred: {e}")
async def main():
while True:
try:
@@ -64,5 +78,6 @@ async def main():
print("Reconnecting in 5 seconds...")
await asyncio.sleep(5)
if __name__ == "__main__":
asyncio.run(main())
asyncio.run(main())
@@ -0,0 +1,262 @@
syntax = "proto3";
import public "solana-storage.proto";
option go_package = "github.com/rpcpool/yellowstone-grpc/examples/golang/proto";
package geyser;
service Geyser {
rpc Subscribe(stream SubscribeRequest) returns (stream SubscribeUpdate) {}
rpc Ping(PingRequest) returns (PongResponse) {}
rpc GetLatestBlockhash(GetLatestBlockhashRequest) returns (GetLatestBlockhashResponse) {}
rpc GetBlockHeight(GetBlockHeightRequest) returns (GetBlockHeightResponse) {}
rpc GetSlot(GetSlotRequest) returns (GetSlotResponse) {}
rpc IsBlockhashValid(IsBlockhashValidRequest) returns (IsBlockhashValidResponse) {}
rpc GetVersion(GetVersionRequest) returns (GetVersionResponse) {}
}
enum CommitmentLevel {
PROCESSED = 0;
CONFIRMED = 1;
FINALIZED = 2;
FIRST_SHRED_RECEIVED = 3;
COMPLETED = 4;
CREATED_BANK = 5;
DEAD = 6;
}
message SubscribeRequest {
map<string, SubscribeRequestFilterAccounts> accounts = 1;
map<string, SubscribeRequestFilterSlots> slots = 2;
map<string, SubscribeRequestFilterTransactions> transactions = 3;
map<string, SubscribeRequestFilterTransactions> transactions_status = 10;
map<string, SubscribeRequestFilterBlocks> blocks = 4;
map<string, SubscribeRequestFilterBlocksMeta> blocks_meta = 5;
map<string, SubscribeRequestFilterEntry> entry = 8;
optional CommitmentLevel commitment = 6;
repeated SubscribeRequestAccountsDataSlice accounts_data_slice = 7;
optional SubscribeRequestPing ping = 9;
}
message SubscribeRequestFilterAccounts {
repeated string account = 2;
repeated string owner = 3;
repeated SubscribeRequestFilterAccountsFilter filters = 4;
optional bool nonempty_txn_signature = 5;
}
message SubscribeRequestFilterAccountsFilter {
oneof filter {
SubscribeRequestFilterAccountsFilterMemcmp memcmp = 1;
uint64 datasize = 2;
bool token_account_state = 3;
SubscribeRequestFilterAccountsFilterLamports lamports = 4;
}
}
message SubscribeRequestFilterAccountsFilterMemcmp {
uint64 offset = 1;
oneof data {
bytes bytes = 2;
string base58 = 3;
string base64 = 4;
}
}
message SubscribeRequestFilterAccountsFilterLamports {
oneof cmp {
uint64 eq = 1;
uint64 ne = 2;
uint64 lt = 3;
uint64 gt = 4;
}
}
message SubscribeRequestFilterSlots {
optional bool filter_by_commitment = 1;
}
message SubscribeRequestFilterTransactions {
optional bool vote = 1;
optional bool failed = 2;
optional string signature = 5;
repeated string account_include = 3;
repeated string account_exclude = 4;
repeated string account_required = 6;
}
message SubscribeRequestFilterBlocks {
repeated string account_include = 1;
optional bool include_transactions = 2;
optional bool include_accounts = 3;
optional bool include_entries = 4;
}
message SubscribeRequestFilterBlocksMeta {}
message SubscribeRequestFilterEntry {}
message SubscribeRequestAccountsDataSlice {
uint64 offset = 1;
uint64 length = 2;
}
message SubscribeRequestPing {
int32 id = 1;
}
message SubscribeUpdate {
repeated string filters = 1;
oneof update_oneof {
SubscribeUpdateAccount account = 2;
SubscribeUpdateSlot slot = 3;
SubscribeUpdateTransaction transaction = 4;
SubscribeUpdateTransactionStatus transaction_status = 10;
SubscribeUpdateBlock block = 5;
SubscribeUpdatePing ping = 6;
SubscribeUpdatePong pong = 9;
SubscribeUpdateBlockMeta block_meta = 7;
SubscribeUpdateEntry entry = 8;
}
}
message SubscribeUpdateAccount {
SubscribeUpdateAccountInfo account = 1;
uint64 slot = 2;
bool is_startup = 3;
}
message SubscribeUpdateAccountInfo {
bytes pubkey = 1;
uint64 lamports = 2;
bytes owner = 3;
bool executable = 4;
uint64 rent_epoch = 5;
bytes data = 6;
uint64 write_version = 7;
optional bytes txn_signature = 8;
}
message SubscribeUpdateSlot {
uint64 slot = 1;
optional uint64 parent = 2;
CommitmentLevel status = 3;
optional string dead_error = 4;
}
message SubscribeUpdateTransaction {
SubscribeUpdateTransactionInfo transaction = 1;
uint64 slot = 2;
}
message SubscribeUpdateTransactionInfo {
bytes signature = 1;
bool is_vote = 2;
solana.storage.ConfirmedBlock.Transaction transaction = 3;
solana.storage.ConfirmedBlock.TransactionStatusMeta meta = 4;
uint64 index = 5;
}
message SubscribeUpdateTransactionStatus {
uint64 slot = 1;
bytes signature = 2;
bool is_vote = 3;
uint64 index = 4;
solana.storage.ConfirmedBlock.TransactionError err = 5;
}
message SubscribeUpdateBlock {
uint64 slot = 1;
string blockhash = 2;
solana.storage.ConfirmedBlock.Rewards rewards = 3;
solana.storage.ConfirmedBlock.UnixTimestamp block_time = 4;
solana.storage.ConfirmedBlock.BlockHeight block_height = 5;
uint64 parent_slot = 7;
string parent_blockhash = 8;
uint64 executed_transaction_count = 9;
repeated SubscribeUpdateTransactionInfo transactions = 6;
uint64 updated_account_count = 10;
repeated SubscribeUpdateAccountInfo accounts = 11;
uint64 entries_count = 12;
repeated SubscribeUpdateEntry entries = 13;
}
message SubscribeUpdateBlockMeta {
uint64 slot = 1;
string blockhash = 2;
solana.storage.ConfirmedBlock.Rewards rewards = 3;
solana.storage.ConfirmedBlock.UnixTimestamp block_time = 4;
solana.storage.ConfirmedBlock.BlockHeight block_height = 5;
uint64 parent_slot = 6;
string parent_blockhash = 7;
uint64 executed_transaction_count = 8;
uint64 entries_count = 9;
}
message SubscribeUpdateEntry {
uint64 slot = 1;
uint64 index = 2;
uint64 num_hashes = 3;
bytes hash = 4;
uint64 executed_transaction_count = 5;
uint64 starting_transaction_index = 6; // added in v1.18, for solana 1.17 value is always 0
}
message SubscribeUpdatePing {}
message SubscribeUpdatePong {
int32 id = 1;
}
// non-streaming methods
message PingRequest {
int32 count = 1;
}
message PongResponse {
int32 count = 1;
}
message GetLatestBlockhashRequest {
optional CommitmentLevel commitment = 1;
}
message GetLatestBlockhashResponse {
uint64 slot = 1;
string blockhash = 2;
uint64 last_valid_block_height = 3;
}
message GetBlockHeightRequest {
optional CommitmentLevel commitment = 1;
}
message GetBlockHeightResponse {
uint64 block_height = 1;
}
message GetSlotRequest {
optional CommitmentLevel commitment = 1;
}
message GetSlotResponse {
uint64 slot = 1;
}
message GetVersionRequest {}
message GetVersionResponse {
string version = 1;
}
message IsBlockhashValidRequest {
string blockhash = 1;
optional CommitmentLevel commitment = 2;
}
message IsBlockhashValidResponse {
uint64 slot = 1;
bool valid = 2;
}
@@ -0,0 +1,149 @@
syntax = "proto3";
package solana.storage.ConfirmedBlock;
option go_package = "github.com/rpcpool/yellowstone-grpc/examples/golang/proto";
message ConfirmedBlock {
string previous_blockhash = 1;
string blockhash = 2;
uint64 parent_slot = 3;
repeated ConfirmedTransaction transactions = 4;
repeated Reward rewards = 5;
UnixTimestamp block_time = 6;
BlockHeight block_height = 7;
NumPartitions num_partitions = 8;
}
message ConfirmedTransaction {
Transaction transaction = 1;
TransactionStatusMeta meta = 2;
}
message Transaction {
repeated bytes signatures = 1;
Message message = 2;
}
message Message {
MessageHeader header = 1;
repeated bytes account_keys = 2;
bytes recent_blockhash = 3;
repeated CompiledInstruction instructions = 4;
bool versioned = 5;
repeated MessageAddressTableLookup address_table_lookups = 6;
}
message MessageHeader {
uint32 num_required_signatures = 1;
uint32 num_readonly_signed_accounts = 2;
uint32 num_readonly_unsigned_accounts = 3;
}
message MessageAddressTableLookup {
bytes account_key = 1;
bytes writable_indexes = 2;
bytes readonly_indexes = 3;
}
message TransactionStatusMeta {
TransactionError err = 1;
uint64 fee = 2;
repeated uint64 pre_balances = 3;
repeated uint64 post_balances = 4;
repeated InnerInstructions inner_instructions = 5;
bool inner_instructions_none = 10;
repeated string log_messages = 6;
bool log_messages_none = 11;
repeated TokenBalance pre_token_balances = 7;
repeated TokenBalance post_token_balances = 8;
repeated Reward rewards = 9;
repeated bytes loaded_writable_addresses = 12;
repeated bytes loaded_readonly_addresses = 13;
ReturnData return_data = 14;
bool return_data_none = 15;
// Sum of compute units consumed by all instructions.
// Available since Solana v1.10.35 / v1.11.6.
// Set to `None` for txs executed on earlier versions.
optional uint64 compute_units_consumed = 16;
}
message TransactionError {
bytes err = 1;
}
message InnerInstructions {
uint32 index = 1;
repeated InnerInstruction instructions = 2;
}
message InnerInstruction {
uint32 program_id_index = 1;
bytes accounts = 2;
bytes data = 3;
// Invocation stack height of an inner instruction.
// Available since Solana v1.14.6
// Set to `None` for txs executed on earlier versions.
optional uint32 stack_height = 4;
}
message CompiledInstruction {
uint32 program_id_index = 1;
bytes accounts = 2;
bytes data = 3;
}
message TokenBalance {
uint32 account_index = 1;
string mint = 2;
UiTokenAmount ui_token_amount = 3;
string owner = 4;
string program_id = 5;
}
message UiTokenAmount {
double ui_amount = 1;
uint32 decimals = 2;
string amount = 3;
string ui_amount_string = 4;
}
message ReturnData {
bytes program_id = 1;
bytes data = 2;
}
enum RewardType {
Unspecified = 0;
Fee = 1;
Rent = 2;
Staking = 3;
Voting = 4;
}
message Reward {
string pubkey = 1;
int64 lamports = 2;
uint64 post_balance = 3;
RewardType reward_type = 4;
string commission = 5;
}
message Rewards {
repeated Reward rewards = 1;
NumPartitions num_partitions = 2;
}
message UnixTimestamp {
int64 timestamp = 1;
}
message BlockHeight {
uint64 block_height = 1;
}
message NumPartitions {
uint64 num_partitions = 1;
}
@@ -1,109 +0,0 @@
import asyncio
import json
import websockets
import base64
import struct
import hashlib
import sys
import os
from solders.transaction import VersionedTransaction
from solders.pubkey import Pubkey
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from config import WSS_ENDPOINT, PUMP_PROGRAM
def load_idl(file_path):
with open(file_path, 'r') as f:
return json.load(f)
def decode_create_instruction(ix_data, ix_def, accounts):
args = {}
offset = 8 # Skip 8-byte discriminator
for arg in ix_def['args']:
if arg['type'] == 'string':
length = struct.unpack_from('<I', ix_data, offset)[0]
offset += 4
value = ix_data[offset:offset+length].decode('utf-8')
offset += length
elif arg['type'] == 'publicKey':
value = base64.b64encode(ix_data[offset:offset+32]).decode('utf-8')
offset += 32
else:
raise ValueError(f"Unsupported type: {arg['type']}")
args[arg['name']] = value
# Add accounts
args['mint'] = str(accounts[0])
args['bondingCurve'] = str(accounts[2])
args['associatedBondingCurve'] = str(accounts[3])
args['user'] = str(accounts[7])
return args
# Here and later all the discriminators are precalculated. See learning-examples/discriminator.py
async def listen_and_decode_create():
idl = load_idl('../idl/pump_fun_idl.json')
create_discriminator = 8576854823835016728
async with websockets.connect(WSS_ENDPOINT) as websocket:
subscription_message = json.dumps({
"jsonrpc": "2.0",
"id": 1,
"method": "blockSubscribe",
"params": [
{"mentionsAccountOrProgram": str(PUMP_PROGRAM)},
{
"commitment": "confirmed",
"encoding": "base64",
"showRewards": False,
"transactionDetails": "full",
"maxSupportedTransactionVersion": 0
}
]
})
await websocket.send(subscription_message)
print(f"Subscribed to blocks mentioning program: {PUMP_PROGRAM}")
while True:
try:
response = await websocket.recv()
data = json.loads(response)
if 'method' in data and data['method'] == 'blockNotification':
if 'params' in data and 'result' in data['params']:
block_data = data['params']['result']
if 'value' in block_data and 'block' in block_data['value']:
block = block_data['value']['block']
if 'transactions' in block:
for tx in block['transactions']:
if isinstance(tx, dict) and 'transaction' in tx:
tx_data_decoded = base64.b64decode(tx['transaction'][0])
transaction = VersionedTransaction.from_bytes(tx_data_decoded)
for ix in transaction.message.instructions:
if str(transaction.message.account_keys[ix.program_id_index]) == str(PUMP_PROGRAM):
ix_data = bytes(ix.data)
discriminator = struct.unpack('<Q', ix_data[:8])[0]
if discriminator == create_discriminator:
create_ix = next(instr for instr in idl['instructions'] if instr['name'] == 'create')
account_keys = [str(transaction.message.account_keys[index]) for index in ix.accounts]
decoded_args = decode_create_instruction(ix_data, create_ix, account_keys)
print(json.dumps(decoded_args, indent=2))
print("--------------------")
elif 'result' in data:
print(f"Subscription confirmed")
else:
print(f"Received unexpected message type: {data.get('method', 'Unknown')}")
except Exception as e:
print(f"An error occurred: {str(e)}")
print(f"Error details: {type(e).__name__}")
import traceback
traceback.print_exc()
print("WebSocket connection closed.")
if __name__ == "__main__":
asyncio.run(listen_and_decode_create())
-119
View File
@@ -1,119 +0,0 @@
import asyncio
import json
import websockets
import base58
import base64
import struct
import sys
import os
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from config import WSS_ENDPOINT, PUMP_PROGRAM
# Load the IDL JSON file
with open('../idl/pump_fun_idl.json', 'r') as f:
idl = json.load(f)
# Extract the "create" instruction definition
create_instruction = next(instr for instr in idl['instructions'] if instr['name'] == 'create')
def parse_create_instruction(data):
if len(data) < 8:
return None
offset = 8
parsed_data = {}
# Parse fields based on CreateEvent structure
fields = [
('name', 'string'),
('symbol', 'string'),
('uri', 'string'),
('mint', 'publicKey'),
('bondingCurve', 'publicKey'),
('user', 'publicKey'),
]
try:
for field_name, field_type in fields:
if field_type == 'string':
length = struct.unpack('<I', data[offset:offset+4])[0]
offset += 4
value = data[offset:offset+length].decode('utf-8')
offset += length
elif field_type == 'publicKey':
value = base58.b58encode(data[offset:offset+32]).decode('utf-8')
offset += 32
parsed_data[field_name] = value
return parsed_data
except:
return None
def print_transaction_details(log_data):
print(f"Signature: {log_data.get('signature')}")
for log in log_data.get('logs', []):
if log.startswith("Program data:"):
try:
data = base58.b58decode(log.split(": ")[1]).decode('utf-8')
print(f"Data: {data}")
except:
pass
async def listen_for_new_tokens():
while True:
try:
async with websockets.connect(WSS_ENDPOINT) as websocket:
subscription_message = json.dumps({
"jsonrpc": "2.0",
"id": 1,
"method": "logsSubscribe",
"params": [
{"mentions": [str(PUMP_PROGRAM)]},
{"commitment": "processed"}
]
})
await websocket.send(subscription_message)
print(f"Listening for new token creations from program: {PUMP_PROGRAM}")
# Wait for subscription confirmation
response = await websocket.recv()
print(f"Subscription response: {response}")
while True:
try:
response = await websocket.recv()
data = json.loads(response)
if 'method' in data and data['method'] == 'logsNotification':
log_data = data['params']['result']['value']
logs = log_data.get('logs', [])
if any("Program log: Instruction: Create" in log for log in logs):
for log in logs:
if "Program data:" in log:
try:
encoded_data = log.split(": ")[1]
decoded_data = base64.b64decode(encoded_data)
parsed_data = parse_create_instruction(decoded_data)
if parsed_data and 'name' in parsed_data:
print("Signature:", log_data.get('signature'))
for key, value in parsed_data.items():
print(f"{key}: {value}")
print("##########################################################################################")
except Exception as e:
print(f"Failed to decode: {log}")
print(f"Error: {str(e)}")
except Exception as e:
print(f"An error occurred while processing message: {e}")
break
except Exception as e:
print(f"Connection error: {e}")
print("Reconnecting in 5 seconds...")
await asyncio.sleep(5)
if __name__ == "__main__":
asyncio.run(listen_for_new_tokens())
@@ -1,146 +0,0 @@
import asyncio
import json
import websockets
import base58
import base64
import struct
import sys
import os
from solders.pubkey import Pubkey
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from config import (
WSS_ENDPOINT,
PUMP_PROGRAM,
SYSTEM_TOKEN_PROGRAM as TOKEN_PROGRAM_ID,
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM as ATA_PROGRAM_ID
)
def find_associated_bonding_curve(mint: Pubkey, bonding_curve: Pubkey) -> Pubkey:
"""
Find the associated bonding curve for a given mint and bonding curve.
This uses the standard ATA derivation.
"""
derived_address, _ = Pubkey.find_program_address(
[
bytes(bonding_curve),
bytes(TOKEN_PROGRAM_ID),
bytes(mint),
],
ATA_PROGRAM_ID
)
return derived_address
# Load the IDL JSON file
with open('../idl/pump_fun_idl.json', 'r') as f:
idl = json.load(f)
# Extract the "create" instruction definition
create_instruction = next(instr for instr in idl['instructions'] if instr['name'] == 'create')
def parse_create_instruction(data):
if len(data) < 8:
return None
offset = 8
parsed_data = {}
# Parse fields based on CreateEvent structure
fields = [
('name', 'string'),
('symbol', 'string'),
('uri', 'string'),
('mint', 'publicKey'),
('bondingCurve', 'publicKey'),
('user', 'publicKey'),
]
try:
for field_name, field_type in fields:
if field_type == 'string':
length = struct.unpack('<I', data[offset:offset+4])[0]
offset += 4
value = data[offset:offset+length].decode('utf-8')
offset += length
elif field_type == 'publicKey':
value = base58.b58encode(data[offset:offset+32]).decode('utf-8')
offset += 32
parsed_data[field_name] = value
return parsed_data
except:
return None
def print_transaction_details(log_data):
print(f"Signature: {log_data.get('signature')}")
for log in log_data.get('logs', []):
if log.startswith("Program data:"):
try:
data = base58.b58decode(log.split(": ")[1]).decode('utf-8')
print(f"Data: {data}")
except:
pass
async def listen_for_new_tokens():
while True:
try:
async with websockets.connect(WSS_ENDPOINT) as websocket:
subscription_message = json.dumps({
"jsonrpc": "2.0",
"id": 1,
"method": "logsSubscribe",
"params": [
{"mentions": [str(PUMP_PROGRAM)]},
{"commitment": "processed"}
]
})
await websocket.send(subscription_message)
print(f"Listening for new token creations from program: {PUMP_PROGRAM}")
# Wait for subscription confirmation
response = await websocket.recv()
print(f"Subscription response: {response}")
while True:
try:
response = await websocket.recv()
data = json.loads(response)
if 'method' in data and data['method'] == 'logsNotification':
log_data = data['params']['result']['value']
logs = log_data.get('logs', [])
if any("Program log: Instruction: Create" in log for log in logs):
for log in logs:
if "Program data:" in log:
try:
encoded_data = log.split(": ")[1]
decoded_data = base64.b64decode(encoded_data)
parsed_data = parse_create_instruction(decoded_data)
if parsed_data and 'name' in parsed_data:
print("Signature:", log_data.get('signature'))
for key, value in parsed_data.items():
print(f"{key}: {value}")
# Calculate associated bonding curve
mint = Pubkey.from_string(parsed_data['mint'])
bonding_curve = Pubkey.from_string(parsed_data['bondingCurve'])
associated_curve = find_associated_bonding_curve(mint, bonding_curve)
print(f"Associated Bonding Curve: {associated_curve}")
print("##########################################################################################")
except Exception as e:
print(f"Failed to decode: {log}")
print(f"Error: {str(e)}")
except Exception as e:
print(f"An error occurred while processing message: {e}")
break
except Exception as e:
print(f"Connection error: {e}")
print("Reconnecting in 5 seconds...")
await asyncio.sleep(5)
if __name__ == "__main__":
asyncio.run(listen_for_new_tokens())
@@ -1,92 +0,0 @@
import websockets
import asyncio
import json
import base64
from solders.pubkey import Pubkey
import sys
import os
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
from config import WSS_ENDPOINT, PUMP_LIQUIDITY_MIGRATOR
def process_initialize2_transaction(data):
"""Process and decode an initialize2 transaction"""
try:
signature = data['transaction']['signatures'][0]
account_keys = data['transaction']['message']['accountKeys']
# Check raydium_amm_idl.json for the account keys
# The token address is typically the 19th account (index 18)
# The liquidity pool address is typically the 3rd account (index 2)
if len(account_keys) > 18:
token_address = account_keys[18]
liquidity_address = account_keys[2]
print(f"\nSignature: {signature}")
print(f"Token Address: {token_address}")
print(f"Liquidity Address: {liquidity_address}")
print("=" * 50)
else:
print(f"\nError: Not enough account keys (found {len(account_keys)})")
except Exception as e:
print(f"\nError: {str(e)}")
async def listen_for_events():
while True:
try:
async with websockets.connect(WSS_ENDPOINT) as websocket:
subscription_message = json.dumps({
"jsonrpc": "2.0",
"id": 1,
"method": "blockSubscribe",
"params": [
{"mentionsAccountOrProgram": str(PUMP_LIQUIDITY_MIGRATOR)},
{
"commitment": "confirmed",
"encoding": "json",
"showRewards": False,
"transactionDetails": "full",
"maxSupportedTransactionVersion": 0
}
]
})
await websocket.send(subscription_message)
response = await websocket.recv()
print(f"Subscription response: {response}")
print("\nListening for Raydium pool initialization events...")
while True:
try:
response = await asyncio.wait_for(websocket.recv(), timeout=30)
data = json.loads(response)
if 'method' in data and data['method'] == 'blockNotification':
if 'params' in data and 'result' in data['params']:
block_data = data['params']['result']
if 'value' in block_data and 'block' in block_data['value']:
block = block_data['value']['block']
if 'transactions' in block:
for tx in block['transactions']:
logs = tx.get('meta', {}).get('logMessages', [])
# Check for initialize2 instruction
for log in logs:
if "Program log: initialize2: InitializeInstruction2" in log:
print("Found initialize2 instruction!")
process_initialize2_transaction(tx)
break
except asyncio.TimeoutError:
print("\nChecking connection...")
print("Connection alive")
continue
except Exception as e:
print(f"\nConnection error: {str(e)}")
print("Retrying in 5 seconds...")
await asyncio.sleep(5)
if __name__ == "__main__":
asyncio.run(listen_for_events())
+207 -111
View File
@@ -1,29 +1,25 @@
import asyncio
import json
import base64
import struct
import base58
import hashlib
import json
import os
import struct
from solana.transaction import Message
import base58
import spl.token.instructions as spl_token
import websockets
from construct import Flag, Int64ul, Struct
from solana.rpc.async_api import AsyncClient
from solana.rpc.commitment import Confirmed
from solana.rpc.types import TxOpts
from solders.pubkey import Pubkey
from solders.keypair import Keypair
from solders.instruction import Instruction, AccountMeta
from solders.system_program import TransferParams, transfer
from solders.transaction import VersionedTransaction, Transaction
from solders.compute_budget import set_compute_unit_price
import spl.token.instructions as spl_token
from solders.instruction import AccountMeta, Instruction
from solders.keypair import Keypair
from solders.message import Message
from solders.pubkey import Pubkey
from solders.transaction import Transaction, VersionedTransaction
from spl.token.instructions import get_associated_token_address
import websockets
import hashlib
from construct import Struct, Int64ul, Flag
# Here and later all the discriminators are precalculated. See learning-examples/discriminator.py
EXPECTED_DISCRIMINATOR = struct.pack("<Q", 6966180631402821399)
TOKEN_DECIMALS = 6
@@ -31,18 +27,23 @@ TOKEN_DECIMALS = 6
# Global constants
PUMP_PROGRAM = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
PUMP_GLOBAL = Pubkey.from_string("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf")
PUMP_EVENT_AUTHORITY = Pubkey.from_string("Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1")
PUMP_EVENT_AUTHORITY = Pubkey.from_string(
"Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1"
)
PUMP_FEE = Pubkey.from_string("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM")
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL")
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
)
SYSTEM_RENT = Pubkey.from_string("SysvarRent111111111111111111111111111111111")
SOL = Pubkey.from_string("So11111111111111111111111111111111111111112")
LAMPORTS_PER_SOL = 1_000_000_000
# RPC ENDPOINTS
RPC_ENDPOINT = "ENTER_YOUR_CHAINSTACK_HTTP_ENDPOINT"
RPC_WEBSOCKET = "ENTER_YOUR_CHAINSTACK_WS_ENDPOINT"
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
RPC_WEBSOCKET = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
class BondingCurveState:
_STRUCT = Struct(
@@ -51,15 +52,18 @@ class BondingCurveState:
"real_token_reserves" / Int64ul,
"real_sol_reserves" / Int64ul,
"token_total_supply" / Int64ul,
"complete" / Flag
"complete" / Flag,
)
def __init__(self, data: bytes) -> None:
parsed = self._STRUCT.parse(data[8:])
self.__dict__.update(parsed)
async def get_pump_curve_state(conn: AsyncClient, curve_address: Pubkey) -> BondingCurveState:
response = await conn.get_account_info(curve_address)
async def get_pump_curve_state(
conn: AsyncClient, curve_address: Pubkey
) -> BondingCurveState:
response = await conn.get_account_info(curve_address, encoding="base64")
if not response.value or not response.value.data:
raise ValueError("Invalid curve state: No data")
@@ -69,14 +73,25 @@ async def get_pump_curve_state(conn: AsyncClient, curve_address: Pubkey) -> Bond
return BondingCurveState(data)
def calculate_pump_curve_price(curve_state: BondingCurveState) -> float:
if curve_state.virtual_token_reserves <= 0 or curve_state.virtual_sol_reserves <= 0:
raise ValueError("Invalid reserve state")
return (curve_state.virtual_sol_reserves / LAMPORTS_PER_SOL) / (curve_state.virtual_token_reserves / 10 ** TOKEN_DECIMALS)
return (curve_state.virtual_sol_reserves / LAMPORTS_PER_SOL) / (
curve_state.virtual_token_reserves / 10**TOKEN_DECIMALS
)
async def buy_token(mint: Pubkey, bonding_curve: Pubkey, associated_bonding_curve: Pubkey, amount: float, slippage: float = 0.25, max_retries=5):
private_key = base58.b58decode("ENTER_PRIVATE_KEY")
async def buy_token(
mint: Pubkey,
bonding_curve: Pubkey,
associated_bonding_curve: Pubkey,
amount: float,
slippage: float = 0.25,
max_retries=5,
):
private_key = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY"))
payer = Keypair.from_bytes(private_key)
async with AsyncClient(RPC_ENDPOINT) as client:
@@ -94,38 +109,54 @@ async def buy_token(mint: Pubkey, bonding_curve: Pubkey, associated_bonding_curv
# Create associated token account with retries
for ata_attempt in range(max_retries):
try:
account_info = await client.get_account_info(associated_token_account)
account_info = await client.get_account_info(associated_token_account, encoding="base64")
if account_info.value is None:
print(f"Creating associated token account (Attempt {ata_attempt + 1})...")
print(
f"Creating associated token account (Attempt {ata_attempt + 1})..."
)
create_ata_ix = spl_token.create_associated_token_account(
payer=payer.pubkey(),
owner=payer.pubkey(),
mint=mint
payer=payer.pubkey(), owner=payer.pubkey(), mint=mint
)
msg = Message([create_ata_ix], payer.pubkey())
tx_ata = await client.send_transaction(
Transaction([payer], msg, (await client.get_latest_blockhash()).value.blockhash),
opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed)
)
await client.confirm_transaction(tx_ata.value, commitment="confirmed")
Transaction(
[payer],
msg,
(await client.get_latest_blockhash()).value.blockhash,
),
opts=TxOpts(
skip_preflight=True, preflight_commitment=Confirmed
),
)
await client.confirm_transaction(
tx_ata.value, commitment="confirmed"
)
print("Associated token account created.")
print(f"Associated token account address: {associated_token_account}")
print(
f"Associated token account address: {associated_token_account}"
)
break
else:
print("Associated token account already exists.")
print(f"Associated token account address: {associated_token_account}")
print(
f"Associated token account address: {associated_token_account}"
)
break
except Exception as e:
print(f"Attempt {ata_attempt + 1} to create associated token account failed: {str(e)}")
print(
f"Attempt {ata_attempt + 1} to create associated token account failed: {e!s}"
)
if ata_attempt < max_retries - 1:
wait_time = 2 ** ata_attempt
wait_time = 2**ata_attempt
print(f"Retrying in {wait_time} seconds...")
await asyncio.sleep(wait_time)
else:
print("Max retries reached. Unable to create associated token account.")
print(
"Max retries reached. Unable to create associated token account."
)
return
# Continue with the buy transaction
@@ -135,28 +166,58 @@ async def buy_token(mint: Pubkey, bonding_curve: Pubkey, associated_bonding_curv
AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False),
AccountMeta(pubkey=PUMP_FEE, is_signer=False, is_writable=True),
AccountMeta(pubkey=mint, is_signer=False, is_writable=False),
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
AccountMeta(pubkey=associated_bonding_curve, is_signer=False, is_writable=True),
AccountMeta(pubkey=associated_token_account, is_signer=False, is_writable=True),
AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True),
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(
pubkey=bonding_curve, is_signer=False, is_writable=True
),
AccountMeta(
pubkey=associated_bonding_curve,
is_signer=False,
is_writable=True,
),
AccountMeta(
pubkey=associated_token_account,
is_signer=False,
is_writable=True,
),
AccountMeta(
pubkey=payer.pubkey(), is_signer=True, is_writable=True
),
AccountMeta(
pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False
),
AccountMeta(
pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False
),
AccountMeta(pubkey=SYSTEM_RENT, is_signer=False, is_writable=False),
AccountMeta(pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False),
AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(
pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False
),
AccountMeta(
pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False
),
]
discriminator = struct.pack("<Q", 16927863322537952870)
data = discriminator + struct.pack("<Q", int(token_amount * 10**6)) + struct.pack("<Q", max_amount_lamports)
data = (
discriminator
+ struct.pack("<Q", int(token_amount * 10**6))
+ struct.pack("<Q", max_amount_lamports)
)
buy_ix = Instruction(PUMP_PROGRAM, data, accounts)
msg = Message([set_compute_unit_price(1_000), buy_ix], payer.pubkey())
tx_buy = await client.send_transaction(
Transaction([payer], msg, (await client.get_latest_blockhash()).value.blockhash),
opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed)
)
Transaction(
[payer],
msg,
(await client.get_latest_blockhash()).value.blockhash,
),
opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed),
)
print(f"Transaction sent: https://explorer.solana.com/tx/{tx_buy.value}")
print(
f"Transaction sent: https://explorer.solana.com/tx/{tx_buy.value}"
)
await client.confirm_transaction(tx_buy.value, commitment="confirmed")
print("Transaction confirmed")
@@ -165,97 +226,129 @@ async def buy_token(mint: Pubkey, bonding_curve: Pubkey, associated_bonding_curv
except Exception as e:
print(f"Attempt {attempt + 1} failed: {str(e)[:50]}")
if attempt < max_retries - 1:
wait_time = 2 ** attempt
wait_time = 2**attempt
print(f"Retrying in {wait_time} seconds...")
await asyncio.sleep(wait_time)
else:
print("Max retries reached. Unable to complete the transaction.")
def load_idl(file_path):
with open(file_path, 'r') as f:
with open(file_path) as f:
return json.load(f)
def calculate_discriminator(instruction_name):
sha = hashlib.sha256()
sha.update(instruction_name.encode('utf-8'))
return struct.unpack('<Q', sha.digest()[:8])[0]
sha.update(instruction_name.encode("utf-8"))
return struct.unpack("<Q", sha.digest()[:8])[0]
def decode_create_instruction(ix_data, ix_def, accounts):
args = {}
offset = 8 # Skip 8-byte discriminator
for arg in ix_def['args']:
if arg['type'] == 'string':
length = struct.unpack_from('<I', ix_data, offset)[0]
for arg in ix_def["args"]:
if arg["type"] == "string":
length = struct.unpack_from("<I", ix_data, offset)[0]
offset += 4
value = ix_data[offset:offset+length].decode('utf-8')
value = ix_data[offset : offset + length].decode("utf-8")
offset += length
elif arg['type'] == 'publicKey':
value = base64.b64encode(ix_data[offset:offset+32]).decode('utf-8')
elif arg["type"] == "publicKey":
value = base64.b64encode(ix_data[offset : offset + 32]).decode("utf-8")
offset += 32
else:
raise ValueError(f"Unsupported type: {arg['type']}")
args[arg['name']] = value
args[arg["name"]] = value
# Add accounts
args['mint'] = str(accounts[0])
args['bondingCurve'] = str(accounts[2])
args['associatedBondingCurve'] = str(accounts[3])
args['user'] = str(accounts[7])
args["mint"] = str(accounts[0])
args["bondingCurve"] = str(accounts[2])
args["associatedBondingCurve"] = str(accounts[3])
args["user"] = str(accounts[7])
return args
async def listen_for_create_transaction():
idl_path = os.path.join(os.path.dirname(__file__), '..', 'idl', 'pump_fun_idl.json')
idl_path = os.path.join(os.path.dirname(__file__), "..", "idl", "pump_fun_idl.json")
idl = load_idl(idl_path)
create_discriminator = calculate_discriminator("global:create")
async with websockets.connect(RPC_WEBSOCKET) as websocket:
subscription_message = json.dumps({
"jsonrpc": "2.0",
"id": 1,
"method": "blockSubscribe",
"params": [
{"mentionsAccountOrProgram": str(PUMP_PROGRAM)},
{
"commitment": "confirmed",
"encoding": "base64",
"showRewards": False,
"transactionDetails": "full",
"maxSupportedTransactionVersion": 0
}
]
})
subscription_message = json.dumps(
{
"jsonrpc": "2.0",
"id": 1,
"method": "blockSubscribe",
"params": [
{"mentionsAccountOrProgram": str(PUMP_PROGRAM)},
{
"commitment": "confirmed",
"encoding": "base64",
"showRewards": False,
"transactionDetails": "full",
"maxSupportedTransactionVersion": 0,
},
],
}
)
await websocket.send(subscription_message)
print(f"Subscribed to blocks mentioning program: {PUMP_PROGRAM}")
while True:
response = await websocket.recv()
data = json.loads(response)
if 'method' in data and data['method'] == 'blockNotification':
if 'params' in data and 'result' in data['params']:
block_data = data['params']['result']
if 'value' in block_data and 'block' in block_data['value']:
block = block_data['value']['block']
if 'transactions' in block:
for tx in block['transactions']:
if isinstance(tx, dict) and 'transaction' in tx:
tx_data_decoded = base64.b64decode(tx['transaction'][0])
transaction = VersionedTransaction.from_bytes(tx_data_decoded)
if "method" in data and data["method"] == "blockNotification":
if "params" in data and "result" in data["params"]:
block_data = data["params"]["result"]
if "value" in block_data and "block" in block_data["value"]:
block = block_data["value"]["block"]
if "transactions" in block:
for tx in block["transactions"]:
if isinstance(tx, dict) and "transaction" in tx:
tx_data_decoded = base64.b64decode(
tx["transaction"][0]
)
transaction = VersionedTransaction.from_bytes(
tx_data_decoded
)
for ix in transaction.message.instructions:
if str(transaction.message.account_keys[ix.program_id_index]) == str(PUMP_PROGRAM):
if str(
transaction.message.account_keys[
ix.program_id_index
]
) == str(PUMP_PROGRAM):
ix_data = bytes(ix.data)
discriminator = struct.unpack('<Q', ix_data[:8])[0]
discriminator = struct.unpack(
"<Q", ix_data[:8]
)[0]
if discriminator == create_discriminator:
create_ix = next(instr for instr in idl['instructions'] if instr['name'] == 'create')
account_keys = [str(transaction.message.account_keys[index]) for index in ix.accounts]
decoded_args = decode_create_instruction(ix_data, create_ix, account_keys)
create_ix = next(
instr
for instr in idl["instructions"]
if instr["name"] == "create"
)
account_keys = [
str(
transaction.message.account_keys[
index
]
)
for index in ix.accounts
]
decoded_args = (
decode_create_instruction(
ix_data, create_ix, account_keys
)
)
return decoded_args
async def main():
print("Waiting for a new token creation...")
token_data = await listen_for_create_transaction()
@@ -266,9 +359,9 @@ async def main():
print(f"Waiting for {sleep_duration_sec} seconds for things to stabilize...")
await asyncio.sleep(sleep_duration_sec)
mint = Pubkey.from_string(token_data['mint'])
bonding_curve = Pubkey.from_string(token_data['bondingCurve'])
associated_bonding_curve = Pubkey.from_string(token_data['associatedBondingCurve'])
mint = Pubkey.from_string(token_data["mint"])
bonding_curve = Pubkey.from_string(token_data["bondingCurve"])
associated_bonding_curve = Pubkey.from_string(token_data["associatedBondingCurve"])
# Fetch the token price
async with AsyncClient(RPC_ENDPOINT) as client:
@@ -276,13 +369,16 @@ async def main():
token_price_sol = calculate_pump_curve_price(curve_state)
# Amount of SOL to spend (adjust as needed)
amount = 0.00001 # 0.00001 SOL
amount = 0.000_001 # 0.00001 SOL
slippage = 0.3 # 30% slippage tolerance
print(f"Bonding curve address: {bonding_curve}")
print(f"Token price: {token_price_sol:.10f} SOL")
print(f"Buying {amount:.6f} SOL worth of the new token with {slippage*100:.1f}% slippage tolerance...")
print(
f"Buying {amount:.6f} SOL worth of the new token with {slippage * 100:.1f}% slippage tolerance..."
)
await buy_token(mint, bonding_curve, associated_bonding_curve, amount, slippage)
if __name__ == "__main__":
asyncio.run(main())
asyncio.run(main())
+94 -44
View File
@@ -1,19 +1,19 @@
import asyncio
import json
import base64
import os
import struct
import base58
from construct import Flag, Int64ul, Struct
from solana.rpc.async_api import AsyncClient
from solana.transaction import Transaction
from solana.rpc.commitment import Confirmed
from solders.pubkey import Pubkey
from solders.keypair import Keypair
from solders.instruction import Instruction, AccountMeta
from solana.rpc.types import TxOpts
from solders.system_program import TransferParams, transfer
from solders.compute_budget import set_compute_unit_price
from solders.instruction import AccountMeta, Instruction
from solders.keypair import Keypair
from solders.message import Message
from solders.pubkey import Pubkey
from solders.transaction import Transaction
from spl.token.instructions import get_associated_token_address
import spl.token.instructions as spl_token
from construct import Struct, Int64ul, Flag
# Here and later all the discriminators are precalculated. See learning-examples/discriminator.py
EXPECTED_DISCRIMINATOR = struct.pack("<Q", 6966180631402821399)
@@ -22,11 +22,15 @@ TOKEN_DECIMALS = 6
# Global constants
PUMP_PROGRAM = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
PUMP_GLOBAL = Pubkey.from_string("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf")
PUMP_EVENT_AUTHORITY = Pubkey.from_string("Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1")
PUMP_EVENT_AUTHORITY = Pubkey.from_string(
"Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1"
)
PUMP_FEE = Pubkey.from_string("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM")
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL")
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
)
SYSTEM_RENT = Pubkey.from_string("SysvarRent111111111111111111111111111111111")
SOL = Pubkey.from_string("So11111111111111111111111111111111111111112")
LAMPORTS_PER_SOL = 1_000_000_000
@@ -34,7 +38,8 @@ UNIT_PRICE = 10_000_000
UNIT_BUDGET = 100_000
# RPC endpoint
RPC_ENDPOINT = "SOLANA_NODE_RPC_ENDPOINT"
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
class BondingCurveState:
_STRUCT = Struct(
@@ -43,15 +48,18 @@ class BondingCurveState:
"real_token_reserves" / Int64ul,
"real_sol_reserves" / Int64ul,
"token_total_supply" / Int64ul,
"complete" / Flag
"complete" / Flag,
)
def __init__(self, data: bytes) -> None:
parsed = self._STRUCT.parse(data[8:])
self.__dict__.update(parsed)
async def get_pump_curve_state(conn: AsyncClient, curve_address: Pubkey) -> BondingCurveState:
response = await conn.get_account_info(curve_address)
async def get_pump_curve_state(
conn: AsyncClient, curve_address: Pubkey
) -> BondingCurveState:
response = await conn.get_account_info(curve_address, encoding="base64")
if not response.value or not response.value.data:
raise ValueError("Invalid curve state: No data")
@@ -61,11 +69,15 @@ async def get_pump_curve_state(conn: AsyncClient, curve_address: Pubkey) -> Bond
return BondingCurveState(data)
def calculate_pump_curve_price(curve_state: BondingCurveState) -> float:
if curve_state.virtual_token_reserves <= 0 or curve_state.virtual_sol_reserves <= 0:
raise ValueError("Invalid reserve state")
return (curve_state.virtual_sol_reserves / LAMPORTS_PER_SOL) / (curve_state.virtual_token_reserves / 10 ** TOKEN_DECIMALS)
return (curve_state.virtual_sol_reserves / LAMPORTS_PER_SOL) / (
curve_state.virtual_token_reserves / 10**TOKEN_DECIMALS
)
async def get_token_balance(conn: AsyncClient, associated_token_account: Pubkey):
response = await conn.get_token_account_balance(associated_token_account)
@@ -73,13 +85,20 @@ async def get_token_balance(conn: AsyncClient, associated_token_account: Pubkey)
return int(response.value.amount)
return 0
async def sell_token(mint: Pubkey, bonding_curve: Pubkey, associated_bonding_curve: Pubkey, slippage: float = 0.25, max_retries=5):
private_key = base58.b58decode("SOLANA_PRIVATE_KEY")
async def sell_token(
mint: Pubkey,
bonding_curve: Pubkey,
associated_bonding_curve: Pubkey,
slippage: float = 0.25,
max_retries=5,
):
private_key = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY"))
payer = Keypair.from_bytes(private_key)
async with AsyncClient(RPC_ENDPOINT) as client:
associated_token_account = get_associated_token_address(payer.pubkey(), mint)
# Get token balance
token_balance = await get_token_balance(client, associated_token_account)
token_balance_decimal = token_balance / 10**TOKEN_DECIMALS
@@ -98,7 +117,7 @@ async def sell_token(mint: Pubkey, bonding_curve: Pubkey, associated_bonding_cur
min_sol_output = float(token_balance_decimal) * float(token_price_sol)
slippage_factor = 1 - slippage
min_sol_output = int((min_sol_output * slippage_factor) * LAMPORTS_PER_SOL)
print(f"Selling {token_balance_decimal} tokens")
print(f"Minimum SOL output: {min_sol_output / LAMPORTS_PER_SOL:.10f} SOL")
@@ -109,29 +128,56 @@ async def sell_token(mint: Pubkey, bonding_curve: Pubkey, associated_bonding_cur
AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False),
AccountMeta(pubkey=PUMP_FEE, is_signer=False, is_writable=True),
AccountMeta(pubkey=mint, is_signer=False, is_writable=False),
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
AccountMeta(pubkey=associated_bonding_curve, is_signer=False, is_writable=True),
AccountMeta(pubkey=associated_token_account, is_signer=False, is_writable=True),
AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True),
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(pubkey=SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False),
AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(
pubkey=bonding_curve, is_signer=False, is_writable=True
),
AccountMeta(
pubkey=associated_bonding_curve,
is_signer=False,
is_writable=True,
),
AccountMeta(
pubkey=associated_token_account,
is_signer=False,
is_writable=True,
),
AccountMeta(
pubkey=payer.pubkey(), is_signer=True, is_writable=True
),
AccountMeta(
pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False
),
AccountMeta(
pubkey=SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM,
is_signer=False,
is_writable=False,
),
AccountMeta(
pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False
),
AccountMeta(
pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False
),
AccountMeta(
pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False
),
]
discriminator = struct.pack("<Q", 12502976635542562355)
data = discriminator + struct.pack("<Q", amount) + struct.pack("<Q", min_sol_output)
data = (
discriminator
+ struct.pack("<Q", amount)
+ struct.pack("<Q", min_sol_output)
)
sell_ix = Instruction(PUMP_PROGRAM, data, accounts)
recent_blockhash = await client.get_latest_blockhash()
transaction = Transaction()
transaction.add(sell_ix)
transaction.recent_blockhash = recent_blockhash.value.blockhash
msg = Message([set_compute_unit_price(1_000), sell_ix], payer.pubkey())
tx = await client.send_transaction(
transaction,
payer,
Transaction(
[payer],
msg,
(await client.get_latest_blockhash()).value.blockhash,
),
opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed),
)
@@ -142,25 +188,29 @@ async def sell_token(mint: Pubkey, bonding_curve: Pubkey, associated_bonding_cur
return # Success, exit the function
except Exception as e:
print(f"Attempt {attempt + 1} failed: {str(e)}")
print(f"Attempt {attempt + 1} failed: {e!s}")
if attempt < max_retries - 1:
wait_time = 2 ** attempt # Exponential backoff
wait_time = 2**attempt # Exponential backoff
print(f"Retrying in {wait_time} seconds...")
await asyncio.sleep(wait_time)
else:
print("Max retries reached. Unable to complete the transaction.")
async def main():
# Replace these with the actual values for the token you want to sell
mint = Pubkey.from_string("EyLuyWV5N1GVSqLLeumFDWYkmmSkLED5s2xqu37Lpump")
bonding_curve = Pubkey.from_string("AGZLYVwmGL9cXCLN4C3ki9hXGix1kXYjr59B2H7jwRMQ")
associated_bonding_curve = Pubkey.from_string("74H2bo2hjNnWgf9oDHzsVsu3mcsoiYYWJ3s9dVnL5erV")
mint = Pubkey.from_string("7aXYndxpkHRU9xg1hyAE6z3X3KYPc2LR3dJMzYTSpump")
bonding_curve = Pubkey.from_string("5UYdCZigGDyAh1doCW8FdnA7VwJTJePayTLCyZkAWMxg")
associated_bonding_curve = Pubkey.from_string(
"BS2RUaPXjQpfzncizvmEfnARZG6SNp1imXnv5USA7PMA"
)
slippage = 0.25 # 25% slippage tolerance
print(f"Bonding curve address: {bonding_curve}")
print(f"Selling tokens with {slippage*100:.1f}% slippage tolerance...")
print(f"Selling tokens with {slippage * 100:.1f}% slippage tolerance...")
await sell_token(mint, bonding_curve, associated_bonding_curve, slippage)
if __name__ == "__main__":
asyncio.run(main())
asyncio.run(main())
@@ -0,0 +1,95 @@
"""
This module provides functionality to:
1. Find market addresses by base mint
2. Fetch and parse market data (including pool addresses) from Pump AMM program accounts
"""
import asyncio
import os
import struct
import base58
from dotenv import load_dotenv
from solana.rpc.async_api import AsyncClient
from solana.rpc.types import MemcmpOpts
from solders.pubkey import Pubkey
load_dotenv()
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
TOKEN_MINT = Pubkey.from_string("35ySx7Rt3RqeTp75QB81FgRvPT5yDY2m5BupsUYDpump")
async def get_market_address_by_base_mint(base_mint_address: Pubkey, amm_program_id: Pubkey):
async with AsyncClient(RPC_ENDPOINT) as client:
base_mint_bytes = bytes(base_mint_address)
# Define the offset for base_mint field
offset = 43
# Create the filter to match the base_mint
filters = [
MemcmpOpts(offset=offset, bytes=base_mint_bytes)
]
# Retrieve the accounts that match the filter
response = await client.get_program_accounts(
amm_program_id, # AMM program ID
encoding="base64",
filters=filters
)
pool_addresses = [account.pubkey for account in response.value]
return pool_addresses[0]
async def get_market_data(market_address: Pubkey):
async with AsyncClient(RPC_ENDPOINT) as client:
response = await client.get_account_info(market_address, encoding="base64")
data = response.value.data
parsed_data = {}
offset = 8
fields = [
("pool_bump", "u8"),
("index", "u16"),
("creator", "pubkey"),
("base_mint", "pubkey"),
("quote_mint", "pubkey"),
("lp_mint", "pubkey"),
("pool_base_token_account", "pubkey"),
("pool_quote_token_account", "pubkey"),
("lp_supply", "u64"),
]
for field_name, field_type in fields:
if field_type == "pubkey":
value = data[offset:offset + 32]
parsed_data[field_name] = base58.b58encode(value).decode("utf-8")
offset += 32
elif field_type in {"u64", "i64"}:
value = struct.unpack("<Q", data[offset:offset + 8])[0] if field_type == "u64" else struct.unpack("<q", data[offset:offset + 8])[0]
parsed_data[field_name] = value
offset += 8
elif field_type == "u16":
value = struct.unpack("<H", data[offset:offset + 2])[0]
parsed_data[field_name] = value
offset += 2
elif field_type == "u8":
value = data[offset]
parsed_data[field_name] = value
offset += 1
return parsed_data
async def main():
market_address = await get_market_address_by_base_mint(TOKEN_MINT, PUMP_AMM_PROGRAM_ID)
print(market_address)
market_data = await get_market_data(market_address)
print(market_data)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,311 @@
"""
This module provides functionality to:
- Find market addresses by token mint.
- Fetch and parse market data from PUMP AMM pools.
- Calculate token prices in AMM pools.
- Create associated token accounts (ATAs) idempotently.
- Sell tokens on the PUMP AMM with slippage protection.
"""
import asyncio
import os
import struct
import base58
from dotenv import load_dotenv
from solana.rpc.async_api import AsyncClient
from solana.rpc.commitment import Confirmed
from solana.rpc.types import MemcmpOpts, TxOpts
from solders.compute_budget import set_compute_unit_limit, set_compute_unit_price
from solders.instruction import AccountMeta, Instruction
from solders.keypair import Keypair
from solders.message import Message
from solders.pubkey import Pubkey
from solders.transaction import VersionedTransaction
from spl.token.instructions import get_associated_token_address
load_dotenv()
# Configuration constants
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
TOKEN_MINT = Pubkey.from_string("8XNrSusWrzYpizYXJb5yeB66AWX1cfQ86SBJFqVTpump")
PRIVATE_KEY = base58.b58decode(os.environ.get("SOLANA_PRIVATE_KEY"))
PAYER = Keypair.from_bytes(PRIVATE_KEY)
SLIPPAGE = 0.25 # Slippage tolerance (25%) - the maximum price movement you'll accept
TOKEN_DECIMALS = 6
SELL_DISCRIMINATOR = bytes.fromhex("33e685a4017f83ad") # Program instruction identifier for the sell function
# Solana system addresses and program IDs
SOL = Pubkey.from_string("So11111111111111111111111111111111111111112")
PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
PUMP_SWAP_GLOBAL_CONFIG = Pubkey.from_string("ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw")
PUMP_PROTOCOL_FEE_RECIPIENT = Pubkey.from_string("7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ")
PUMP_PROTOCOL_FEE_RECIPIENT_TOKEN_ACCOUNT = Pubkey.from_string("7GFUN3bWzJMKMRZ34JLsvcqdssDbXnp589SiE33KVwcC")
SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL")
PUMP_SWAP_EVENT_AUTHORITY = Pubkey.from_string("GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR")
LAMPORTS_PER_SOL = 1_000_000_000
COMPUTE_UNIT_PRICE = 10_000 # Price in micro-lamports per compute unit
COMPUTE_UNIT_BUDGET = 100_000 # Maximum compute units to use
async def get_market_address_by_base_mint(client: AsyncClient, base_mint_address: Pubkey, amm_program_id: Pubkey) -> Pubkey:
"""Find the market address for a given token mint.
Searches for the AMM pool that contains the specified token as its base token.
Args:
client: Solana RPC client instance
base_mint_address: Address of the token mint you want to find the market for
amm_program_id: Address of the AMM program
Returns:
The Pubkey of the market (AMM pool) for the token
"""
base_mint_bytes = bytes(base_mint_address)
offset = 43 # Offset where the base_mint field is stored in the account data structure
filters = [
MemcmpOpts(offset=offset, bytes=base_mint_bytes)
]
response = await client.get_program_accounts(
amm_program_id,
encoding="base64",
filters=filters
)
market_address = [account.pubkey for account in response.value][0]
return market_address
async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict:
"""Fetch and parse market data from the blockchain.
Retrieves and deserializes the data stored in the market account.
Args:
client: Solana RPC client instance
market_address: Address of the market (AMM pool) to fetch data for
Returns:
Dictionary containing the parsed market data
"""
response = await client.get_account_info(market_address, encoding="base64")
data = response.value.data
parsed_data: dict = {}
# Start after the 8-byte discriminator
offset = 8
# Define the structure of the market account data
fields = [
("pool_bump", "u8"),
("index", "u16"),
("creator", "pubkey"),
("base_mint", "pubkey"),
("quote_mint", "pubkey"),
("lp_mint", "pubkey"),
("pool_base_token_account", "pubkey"),
("pool_quote_token_account", "pubkey"),
("lp_supply", "u64"),
]
for field_name, field_type in fields:
if field_type == "pubkey":
value = data[offset:offset + 32]
parsed_data[field_name] = base58.b58encode(value).decode("utf-8")
offset += 32
elif field_type in {"u64", "i64"}:
value = struct.unpack("<Q", data[offset:offset + 8])[0] if field_type == "u64" else struct.unpack("<q", data[offset:offset + 8])[0]
parsed_data[field_name] = value
offset += 8
elif field_type == "u16":
value = struct.unpack("<H", data[offset:offset + 2])[0]
parsed_data[field_name] = value
offset += 2
elif field_type == "u8":
value = data[offset]
parsed_data[field_name] = value
offset += 1
return parsed_data
async def calculate_token_pool_price(client: AsyncClient, pool_base_token_account: Pubkey, pool_quote_token_account: Pubkey) -> float:
"""Calculate the price of tokens in the pool.
Fetches the balance of tokens in the pool and calculates the price ratio.
Args:
client: Solana RPC client instance
pool_base_token_account: Address of the pool's base token account (your token)
pool_quote_token_account: Address of the pool's quote token account (SOL)
Returns:
The price of the base token in terms of the quote token (usually SOL)
"""
base_balance_resp = await client.get_token_account_balance(pool_base_token_account)
quote_balance_resp = await client.get_token_account_balance(pool_quote_token_account)
# Extract the UI amounts (human-readable with decimals)
base_amount = float(base_balance_resp.value.ui_amount)
quote_amount = float(quote_balance_resp.value.ui_amount)
token_price = quote_amount / base_amount
return token_price
def create_ata_idempotent_ix(payer_pubkey: Pubkey) -> Instruction:
"""Create an instruction to create an Associated Token Account (ATA) if it doesn't exist.
This creates an instruction that will create an Associated Token Account for SOL
if it doesn't already exist.
Args:
payer_pubkey: The public key of the account that will pay for the creation
Returns:
An instruction to create the ATA
"""
associated_token_address = get_associated_token_address(payer_pubkey, SOL)
instruction_accounts = [
AccountMeta(pubkey=payer_pubkey, is_signer=True, is_writable=True),
AccountMeta(pubkey=associated_token_address, is_signer=False, is_writable=True),
AccountMeta(pubkey=payer_pubkey, is_signer=True, is_writable=True),
AccountMeta(pubkey=SOL, is_signer=False, is_writable=False),
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
]
# The data for creating an ATA idempotently is just a single byte with value 1
# Check the details here:
# https://github.com/solana-program/associated-token-account/blob/main/program/src/instruction.rs
data = bytes([1])
return Instruction(SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, data, instruction_accounts)
async def sell_pump_swap(client: AsyncClient, pump_fun_amm_market: Pubkey, payer: Keypair,
base_mint: Pubkey, user_base_token_account: Pubkey,
user_quote_token_account: Pubkey, pool_base_token_account: Pubkey,
pool_quote_token_account: Pubkey, slippage: float = 0.25) -> str | None:
"""Sell tokens on the PUMP AMM.
This function sells all tokens in the user's token account with the specified slippage tolerance.
Args:
client: Solana RPC client instance
pump_fun_amm_market: Address of the AMM market
payer: Keypair of the transaction signer and token seller
base_mint: Address of the token mint being sold
user_base_token_account: Address of the user's token account for the token being sold
user_quote_token_account: Address of the user's SOL token account
pool_base_token_account: Address of the pool's token account for the token being sold
pool_quote_token_account: Address of the pool's SOL token account
slippage: Maximum acceptable price slippage, as a decimal (0.25 = 25%)
Returns:
Transaction signature if successful, None otherwise
"""
# Get token balance
token_balance = int((await client.get_token_account_balance(user_base_token_account)).value.amount)
token_balance_decimal = token_balance / 10**TOKEN_DECIMALS
print(f"Token balance: {token_balance_decimal}")
if token_balance == 0:
print("No tokens to sell.")
return None
# Calculate token price
token_price_sol = await calculate_token_pool_price(client, pool_base_token_account, pool_quote_token_account)
print(f"Price per Token: {token_price_sol:.20f} SOL")
# Calculate minimum SOL output with slippage protection
amount = token_balance
min_sol_output = float(token_balance_decimal) * float(token_price_sol)
slippage_factor = 1 - slippage
min_sol_output = int((min_sol_output * slippage_factor) * LAMPORTS_PER_SOL)
print(f"Selling {token_balance_decimal} tokens")
print(f"Minimum SOL output: {min_sol_output / LAMPORTS_PER_SOL:.10f} SOL")
# Define all accounts needed for the sell instruction
accounts = [
AccountMeta(pubkey=pump_fun_amm_market, is_signer=False, is_writable=False),
AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True),
AccountMeta(pubkey=PUMP_SWAP_GLOBAL_CONFIG, is_signer=False, is_writable=True),
AccountMeta(pubkey=base_mint, is_signer=False, is_writable=False),
AccountMeta(pubkey=SOL, is_signer=False, is_writable=False),
AccountMeta(pubkey=user_base_token_account, is_signer=False, is_writable=True),
AccountMeta(pubkey=user_quote_token_account, is_signer=False, is_writable=True),
AccountMeta(pubkey=pool_base_token_account, is_signer=False, is_writable=True),
AccountMeta(pubkey=pool_quote_token_account, is_signer=False, is_writable=True),
AccountMeta(pubkey=PUMP_PROTOCOL_FEE_RECIPIENT, is_signer=False, is_writable=False),
AccountMeta(pubkey=PUMP_PROTOCOL_FEE_RECIPIENT_TOKEN_ACCOUNT, is_signer=False, is_writable=True),
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(pubkey=SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(pubkey=PUMP_SWAP_EVENT_AUTHORITY, is_signer=False, is_writable=False),
AccountMeta(pubkey=PUMP_AMM_PROGRAM_ID, is_signer=False, is_writable=False),
]
data = SELL_DISCRIMINATOR + struct.pack("<Q", amount) + struct.pack("<Q", min_sol_output)
compute_limit_ix = set_compute_unit_limit(COMPUTE_UNIT_BUDGET)
compute_price_ix = set_compute_unit_price(COMPUTE_UNIT_PRICE)
create_ata_ix = create_ata_idempotent_ix(
payer_pubkey=payer.pubkey(),
)
sell_ix = Instruction(PUMP_AMM_PROGRAM_ID, data, accounts)
blockhash_resp = await client.get_latest_blockhash()
recent_blockhash = blockhash_resp.value.blockhash
msg = Message.new_with_blockhash(
[compute_limit_ix, compute_price_ix, create_ata_ix, sell_ix],
payer.pubkey(),
recent_blockhash
)
tx_sell = VersionedTransaction(
message=msg,
keypairs=[payer]
)
try:
tx_sig = await client.send_transaction(
tx_sell,
opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed),
)
tx_hash = tx_sig.value
print(f"Transaction sent: https://explorer.solana.com/tx/{tx_hash}")
await client.confirm_transaction(tx_hash, commitment="confirmed")
print("Transaction confirmed")
return tx_hash
except Exception as e:
print(f"Error sending transaction: {e!s}")
return None
async def main():
"""Main function to execute the token selling process."""
async with AsyncClient(RPC_ENDPOINT) as client:
market_address = await get_market_address_by_base_mint(client, TOKEN_MINT, PUMP_AMM_PROGRAM_ID)
market_data = await get_market_data(client, market_address)
await sell_pump_swap(
client,
market_address,
PAYER,
TOKEN_MINT,
get_associated_token_address(PAYER.pubkey(), TOKEN_MINT),
get_associated_token_address(PAYER.pubkey(), SOL),
Pubkey.from_string(market_data["pool_base_token_account"]),
Pubkey.from_string(market_data["pool_quote_token_account"]),
SLIPPAGE
)
if __name__ == "__main__":
asyncio.run(main())
+87
View File
@@ -0,0 +1,87 @@
[project]
name = "pump_bot"
version = "2.0"
description = "Trade tokens on pump.fun"
readme = "README.md"
requires-python = ">=3.9"
dependencies = [
"base58>=2.1.1",
"borsh-construct>=0.1.0",
"construct>=2.10.67",
"construct-typing>=0.5.2",
"solana==0.36.6",
"solders>=0.26.0",
"websockets>=15.0",
"python-dotenv>=1.0.1",
"aiohttp>=3.11.13",
"grpcio>=1.71.0",
"grpcio-tools>=1.71.0",
"protobuf>=5.29.4",
"pyyaml>=6.0.2",
"uvloop>=0.21.0",
]
[project.optional-dependencies]
dev = [
"ruff>=0.10.0"
]
[project.scripts]
pump_bot = "bot_runner:main"
[tool.ruff]
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".ipynb_checkpoints",
".mypy_cache",
".nox",
".pants.d",
".pyenv",
".pytest_cache",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
".vscode",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"site-packages",
"venv",
]
line-length = 88
indent-width = 4
target-version = "py311"
[tool.ruff.lint]
select = [
"E", "F", "I", "UP", "N", "B", "A", "C4", "T10", "ARG", "PTH",
"ANN", # type annotations
"S", # security best practices
"BLE", # blind except statements
"FBT", # boolean trap parameters
"C90", # complexity metrics
"TRY", # exception handling best practices
"SLF", # private member access
"TCH", # type checking issues
"RUF", # Ruff-specific rules
"ERA", # eradicate commented-out code
"PL", # pylint conventions
]
ignore = ["E501"]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
line-ending = "auto"
-7
View File
@@ -1,7 +0,0 @@
base58>=2.1.1
borsh-construct>=0.1.0
construct>=2.10.68
construct-typing>=0.5.6
solana==0.34.3
solders>=0.21.0
websockets>=10.4
-141
View File
@@ -1,141 +0,0 @@
import asyncio
import json
import base64
import struct
import base58
from typing import Final
from solana.rpc.async_api import AsyncClient
from solana.transaction import Transaction
from solana.rpc.commitment import Confirmed
from solana.rpc.types import TxOpts
from solders.pubkey import Pubkey
from solders.keypair import Keypair
from solders.instruction import Instruction, AccountMeta
from solders.system_program import TransferParams, transfer
from spl.token.instructions import get_associated_token_address
import spl.token.instructions as spl_token
from construct import Struct, Int64ul, Flag
from config import *
# Here and later all the discriminators are precalculated. See learning-examples/discriminator.py
EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack("<Q", 6966180631402821399)
TOKEN_DECIMALS: Final[int] = 6
class BondingCurveState:
_STRUCT = Struct(
"virtual_token_reserves" / Int64ul,
"virtual_sol_reserves" / Int64ul,
"real_token_reserves" / Int64ul,
"real_sol_reserves" / Int64ul,
"token_total_supply" / Int64ul,
"complete" / Flag
)
def __init__(self, data: bytes) -> None:
parsed = self._STRUCT.parse(data[8:])
self.__dict__.update(parsed)
async def get_pump_curve_state(conn: AsyncClient, curve_address: Pubkey) -> BondingCurveState:
response = await conn.get_account_info(curve_address)
if not response.value or not response.value.data:
raise ValueError("Invalid curve state: No data")
data = response.value.data
if data[:8] != EXPECTED_DISCRIMINATOR:
raise ValueError("Invalid curve state discriminator")
return BondingCurveState(data)
def calculate_pump_curve_price(curve_state: BondingCurveState) -> float:
if curve_state.virtual_token_reserves <= 0 or curve_state.virtual_sol_reserves <= 0:
raise ValueError("Invalid reserve state")
return (curve_state.virtual_sol_reserves / LAMPORTS_PER_SOL) / (curve_state.virtual_token_reserves / 10 ** TOKEN_DECIMALS)
async def get_token_balance(conn: AsyncClient, associated_token_account: Pubkey):
response = await conn.get_token_account_balance(associated_token_account)
if response.value:
return int(response.value.amount)
return 0
async def sell_token(mint: Pubkey, bonding_curve: Pubkey, associated_bonding_curve: Pubkey, slippage: float = 0.25, max_retries=5):
private_key = base58.b58decode(PRIVATE_KEY)
payer = Keypair.from_bytes(private_key)
async with AsyncClient(RPC_ENDPOINT) as client:
associated_token_account = get_associated_token_address(payer.pubkey(), mint)
# Get token balance
token_balance = await get_token_balance(client, associated_token_account)
token_balance_decimal = token_balance / 10**TOKEN_DECIMALS
print(f"Token balance: {token_balance_decimal}")
if token_balance == 0:
print("No tokens to sell.")
return
# Fetch the token price
curve_state = await get_pump_curve_state(client, bonding_curve)
token_price_sol = calculate_pump_curve_price(curve_state)
print(f"Price per Token: {token_price_sol:.20f} SOL")
# Calculate minimum SOL output
amount = token_balance
min_sol_output = float(token_balance_decimal) * float(token_price_sol)
slippage_factor = 1 - slippage
min_sol_output = int((min_sol_output * slippage_factor) * LAMPORTS_PER_SOL)
print(f"Selling {token_balance_decimal} tokens")
print(f"Minimum SOL output: {min_sol_output / LAMPORTS_PER_SOL:.10f} SOL")
for attempt in range(max_retries):
try:
accounts = [
AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False),
AccountMeta(pubkey=PUMP_FEE, is_signer=False, is_writable=True),
AccountMeta(pubkey=mint, is_signer=False, is_writable=False),
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
AccountMeta(pubkey=associated_bonding_curve, is_signer=False, is_writable=True),
AccountMeta(pubkey=associated_token_account, is_signer=False, is_writable=True),
AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True),
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(pubkey=SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False),
AccountMeta(pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False),
AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False),
]
discriminator = struct.pack("<Q", 12502976635542562355)
data = discriminator + struct.pack("<Q", amount) + struct.pack("<Q", min_sol_output)
sell_ix = Instruction(PUMP_PROGRAM, data, accounts)
recent_blockhash = await client.get_latest_blockhash()
transaction = Transaction()
transaction.add(sell_ix)
transaction.recent_blockhash = recent_blockhash.value.blockhash
tx = await client.send_transaction(
transaction,
payer,
opts=TxOpts(skip_preflight=True, preflight_commitment=Confirmed),
)
print(f"Transaction sent: https://explorer.solana.com/tx/{tx.value}")
await client.confirm_transaction(tx.value, commitment="confirmed")
print("Transaction confirmed")
return tx.value
except Exception as e:
print(f"Attempt {attempt + 1} failed: {str(e)}")
if attempt < max_retries - 1:
wait_time = 2 ** attempt
print(f"Retrying in {wait_time} seconds...")
await asyncio.sleep(wait_time)
else:
print("Max retries reached. Unable to complete the transaction.")
View File
+156
View File
@@ -0,0 +1,156 @@
import asyncio
import logging
import multiprocessing
from datetime import datetime
from pathlib import Path
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
from config_loader import load_bot_config, print_config_summary
from trading.trader import PumpTrader
from utils.logger import setup_file_logging
def setup_logging(bot_name: str):
"""
Set up logging to file for a specific bot instance.
Args:
bot_name: Name of the bot for the log file
"""
log_dir = Path("logs")
log_dir.mkdir(exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
log_filename = log_dir / f"{bot_name}_{timestamp}.log"
setup_file_logging(str(log_filename))
async def start_bot(config_path: str):
"""
Start a trading bot with the configuration from the specified path.
Args:
config_path: Path to the YAML configuration file
"""
cfg = load_bot_config(config_path)
setup_logging(cfg["name"])
print_config_summary(cfg)
trader = PumpTrader(
# Connection settings
rpc_endpoint=cfg["rpc_endpoint"],
wss_endpoint=cfg["wss_endpoint"],
private_key=cfg["private_key"],
# Trade parameters
buy_amount=cfg["trade"]["buy_amount"],
buy_slippage=cfg["trade"]["buy_slippage"],
sell_slippage=cfg["trade"]["sell_slippage"],
# Extreme fast mode settings
extreme_fast_mode=cfg["trade"].get("extreme_fast_mode", False),
extreme_fast_token_amount=cfg["trade"].get("extreme_fast_token_amount", 30),
# Listener configuration
listener_type=cfg["filters"]["listener_type"],
# Geyser configuration (if applicable)
geyser_endpoint=cfg.get("geyser", {}).get("endpoint"),
geyser_api_token=cfg.get("geyser", {}).get("api_token"),
# Priority fee configuration
enable_dynamic_priority_fee=cfg.get("priority_fees", {}).get("enable_dynamic", False),
enable_fixed_priority_fee=cfg.get("priority_fees", {}).get("enable_fixed", True),
fixed_priority_fee=cfg.get("priority_fees", {}).get("fixed_amount", 500000),
extra_priority_fee=cfg.get("priority_fees", {}).get("extra_percentage", 0.0),
hard_cap_prior_fee=cfg.get("priority_fees", {}).get("hard_cap", 500000),
# Retry and timeout settings
max_retries=cfg.get("retries", {}).get("max_attempts", 10),
wait_time_after_creation=cfg.get("retries", {}).get("wait_after_creation", 15),
wait_time_after_buy=cfg.get("retries", {}).get("wait_after_buy", 15),
wait_time_before_new_token=cfg.get("retries", {}).get("wait_before_new_token", 15),
max_token_age=cfg.get("timing", {}).get("max_token_age", 0.001),
token_wait_timeout=cfg.get("timing", {}).get("token_wait_timeout", 30),
# Cleanup settings
cleanup_mode=cfg.get("cleanup", {}).get("mode", "disabled"),
cleanup_force_close_with_burn=cfg.get("cleanup", {}).get("force_close_with_burn", False),
cleanup_with_priority_fee=cfg.get("cleanup", {}).get("with_priority_fee", False),
# Trading filters
match_string=cfg["filters"].get("match_string"),
bro_address=cfg["filters"].get("bro_address"),
marry_mode=cfg["filters"].get("marry_mode", False),
yolo_mode=cfg["filters"].get("yolo_mode", False),
)
await trader.start()
def run_all_bots():
"""
Run all bots defined in YAML files in the 'bots' directory.
Only runs bots that have enabled=True (or where enabled is not specified).
Bots can be run in separate processes based on their configuration.
"""
bot_dir = Path("bots")
if not bot_dir.exists():
logging.error(f"Bot directory '{bot_dir}' not found")
return
bot_files = list(bot_dir.glob("*.yaml"))
if not bot_files:
logging.error(f"No bot configuration files found in '{bot_dir}'")
return
logging.info(f"Found {len(bot_files)} bot configuration files")
processes = []
skipped_bots = 0
for file in bot_files:
try:
cfg = load_bot_config(str(file))
bot_name = cfg.get("name", file.stem)
# Skip bots with enabled=False
if not cfg.get("enabled", True):
logging.info(f"Skipping disabled bot '{bot_name}'")
skipped_bots += 1
continue
if cfg.get("separate_process", False):
logging.info(f"Starting bot '{bot_name}' in separate process")
p = multiprocessing.Process(
target=lambda path=str(file): asyncio.run(start_bot(path)),
name=f"bot-{bot_name}"
)
p.start()
processes.append(p)
else:
logging.info(f"Starting bot '{bot_name}' in main process")
asyncio.run(start_bot(str(file)))
except Exception as e:
logging.exception(f"Failed to start bot from {file}: {e}")
logging.info(f"Started {len(bot_files) - skipped_bots} bots, skipped {skipped_bots} disabled bots")
for p in processes:
p.join()
logging.info(f"Process {p.name} completed")
def main() -> None:
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
run_all_bots()
if __name__ == "__main__":
main()
View File
+106
View File
@@ -0,0 +1,106 @@
import asyncio
from solders.pubkey import Pubkey
from spl.token.instructions import BurnParams, CloseAccountParams, burn, close_account
from core.client import SolanaClient
from core.priority_fee.manager import PriorityFeeManager
from core.pubkeys import SystemAddresses
from core.wallet import Wallet
from utils.logger import get_logger
logger = get_logger(__name__)
class AccountCleanupManager:
"""Handles safe cleanup of token accounts (ATA) after trading sessions."""
def __init__(
self,
client: SolanaClient,
wallet: Wallet,
priority_fee_manager: PriorityFeeManager,
use_priority_fee: bool = False,
force_burn: bool = False,
):
"""
Args:
client: Solana RPC client
wallet: Wallet for signing transactions
"""
self.client = client
self.wallet = wallet
self.priority_fee_manager = priority_fee_manager
self.use_priority_fee = use_priority_fee
self.close_with_force_burn = force_burn
async def cleanup_ata(self, mint: Pubkey) -> None:
"""
Attempt to burn any remaining tokens and close the ATA.
Skips if account doesn't exist or is already empty/closed.
"""
ata = self.wallet.get_associated_token_address(mint)
solana_client = await self.client.get_client()
priority_fee = (
await self.priority_fee_manager.calculate_priority_fee([ata])
if self.use_priority_fee
else None
)
logger.info("Waiting for 15 seconds for RPC node to synchronize...")
await asyncio.sleep(15)
try:
info = await solana_client.get_account_info(ata, encoding="base64")
if not info.value:
logger.info(f"ATA {ata} does not exist or already closed.")
return
balance = await self.client.get_token_account_balance(ata)
instructions = []
if balance > 0 and self.close_with_force_burn:
logger.info(f"Burning {balance} tokens from ATA {ata} (mint: {mint})...")
burn_ix = burn(
BurnParams(
account=ata,
mint=mint,
owner=self.wallet.pubkey,
amount=balance,
program_id=SystemAddresses.TOKEN_PROGRAM,
)
)
instructions.append(burn_ix)
elif balance > 0:
logger.info(
f"Skipping ATA {ata} with non-zero balance ({balance} tokens) "
f"because CLEANUP_FORCE_CLOSE_WITH_BURN is disabled."
)
return
# Include close account instruction
logger.info(f"Closing ATA: {ata}")
close_ix = close_account(
CloseAccountParams(
account=ata,
dest=self.wallet.pubkey,
owner=self.wallet.pubkey,
program_id=SystemAddresses.TOKEN_PROGRAM,
)
)
instructions.append(close_ix)
# Send both burn and close instructions in the same transaction
if instructions:
tx_sig = await self.client.build_and_send_transaction(
instructions,
self.wallet.keypair,
skip_preflight=True,
priority_fee=priority_fee,
)
await self.client.confirm_transaction(tx_sig)
logger.info(f"Closed successfully: {ata}")
except Exception as e:
logger.warning(f"Cleanup failed for ATA {ata}: {e!s}")
+42
View File
@@ -0,0 +1,42 @@
from cleanup.manager import AccountCleanupManager
from utils.logger import get_logger
logger = get_logger(__name__)
def should_cleanup_after_failure(cleanup_mode) -> bool:
return cleanup_mode == "on_fail"
def should_cleanup_after_sell(cleanup_mode) -> bool:
return cleanup_mode == "after_sell"
def should_cleanup_post_session(cleanup_mode) -> bool:
return cleanup_mode == "post_session"
async def handle_cleanup_after_failure(
client, wallet, mint, priority_fee_manager, cleanup_mode, cleanup_with_prior_fee, force_burn
):
if should_cleanup_after_failure(cleanup_mode):
logger.info("[Cleanup] Triggered by failed buy transaction.")
manager = AccountCleanupManager(client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn)
await manager.cleanup_ata(mint)
async def handle_cleanup_after_sell(
client, wallet, mint, priority_fee_manager, cleanup_mode, cleanup_with_prior_fee, force_burn
):
if should_cleanup_after_sell(cleanup_mode):
logger.info("[Cleanup] Triggered after token sell.")
manager = AccountCleanupManager(client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn)
await manager.cleanup_ata(mint)
async def handle_cleanup_post_session(
client, wallet, mints, priority_fee_manager, cleanup_mode, cleanup_with_prior_fee, force_burn
):
if should_cleanup_post_session(cleanup_mode):
logger.info("[Cleanup] Triggered post trading session.")
manager = AccountCleanupManager(client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn)
for mint in mints:
await manager.cleanup_ata(mint)
+188
View File
@@ -0,0 +1,188 @@
import os
from typing import Any
import yaml
from dotenv import load_dotenv
REQUIRED_FIELDS = [
"name", "rpc_endpoint", "wss_endpoint", "private_key",
"trade.buy_amount", "trade.buy_slippage", "trade.sell_slippage",
"filters.listener_type", "filters.max_token_age"
]
CONFIG_VALIDATION_RULES = [
# (path, type, min_value, max_value, error_message)
("trade.buy_amount", (int, float), 0, float('inf'), "trade.buy_amount must be a positive number"),
("trade.buy_slippage", float, 0, 1, "trade.buy_slippage must be between 0 and 1"),
("trade.sell_slippage", float, 0, 1, "trade.sell_slippage must be between 0 and 1"),
("priority_fees.fixed_amount", int, 0, float('inf'), "priority_fees.fixed_amount must be a non-negative integer"),
("priority_fees.extra_percentage", float, 0, 1, "priority_fees.extra_percentage must be between 0 and 1"),
("priority_fees.hard_cap", int, 0, float('inf'), "priority_fees.hard_cap must be a non-negative integer"),
("retries.max_attempts", int, 0, 100, "retries.max_attempts must be between 0 and 100"),
("filters.max_token_age", (int, float), 0, float('inf'), "filters.max_token_age must be a non-negative number")
]
# Valid values for enum-like fields
VALID_VALUES = {
"filters.listener_type": ["logs", "blocks", "geyser"],
"cleanup.mode": ["disabled", "on_fail", "after_sell", "post_session"]
}
def load_bot_config(path: str) -> dict:
"""
Load and validate a bot configuration from a YAML file.
Args:
path: Path to the YAML configuration file (relative or absolute)
Returns:
Validated configuration dictionary
Raises:
FileNotFoundError: If the configuration file doesn't exist
ValueError: If the configuration is invalid
"""
with open(path) as f:
config = yaml.safe_load(f)
env_file = config.get("env_file")
if env_file:
env_path = os.path.join(os.path.dirname(path), env_file)
if os.path.exists(env_path):
load_dotenv(env_path, override=True)
else:
# If not found relative to config, try relative to current working directory
load_dotenv(env_file, override=True)
resolve_env_vars(config)
validate_config(config)
return config
def resolve_env_vars(config: dict) -> None:
"""
Recursively resolve environment variables in the configuration.
Args:
config: Configuration dictionary to process
"""
def resolve_env(value):
if isinstance(value, str) and value.startswith("${") and value.endswith("}"):
env_var = value[2:-1]
env_value = os.getenv(env_var)
if env_value is None:
raise ValueError(f"Environment variable '{env_var}' not found")
return env_value
return value
def resolve_all(d):
for k, v in d.items():
if isinstance(v, dict):
resolve_all(v)
else:
d[k] = resolve_env(v)
resolve_all(config)
def get_nested_value(config: dict, path: str) -> Any:
"""
Get a nested value from the configuration using dot notation.
Args:
config: Configuration dictionary
path: Path to the value using dot notation (e.g., "trade.buy_amount")
Returns:
The value at the specified path
Raises:
ValueError: If the path doesn't exist in the configuration
"""
keys = path.split(".")
value = config
for key in keys:
if not isinstance(value, dict) or key not in value:
raise ValueError(f"Missing required config key: {path}")
value = value[key]
return value
def validate_config(config: dict) -> None:
"""
Validate the configuration against defined rules.
Args:
config: Configuration dictionary to validate
Raises:
ValueError: If the configuration is invalid
"""
for field in REQUIRED_FIELDS:
get_nested_value(config, field)
for path, expected_type, min_val, max_val, error_msg in CONFIG_VALIDATION_RULES:
try:
value = get_nested_value(config, path)
if not isinstance(value, expected_type):
raise ValueError(f"Type error: {error_msg}")
if isinstance(value, (int, float)) and not (min_val <= value <= max_val):
raise ValueError(f"Range error: {error_msg}")
except ValueError as e:
# Re-raise if it's our own error
if str(e).startswith(("Type error:", "Range error:")):
raise
# Otherwise, the field might be missing
continue
# Validate enum-like fields
for path, valid_values in VALID_VALUES.items():
try:
value = get_nested_value(config, path)
if value not in valid_values:
raise ValueError(f"{path} must be one of {valid_values}")
except ValueError:
# Skip if the field is missing
continue
# Cannot enable both dynamic and fixed priority fees
try:
dynamic = get_nested_value(config, "priority_fees.enable_dynamic")
fixed = get_nested_value(config, "priority_fees.enable_fixed")
if dynamic and fixed:
raise ValueError("Cannot enable both dynamic and fixed priority fees simultaneously")
except ValueError:
# Skip if one of the fields is missing
pass
def print_config_summary(config: dict) -> None:
"""
Print a summary of the loaded configuration.
Args:
config: Configuration dictionary
"""
print(f"Bot name: {config.get('name', 'unnamed')}")
print(f"Listener type: {config.get('filters', {}).get('listener_type', 'not configured')}")
trade = config.get('trade', {})
print("Trade settings:")
print(f" - Buy amount: {trade.get('buy_amount', 'not configured')} SOL")
print(f" - Buy slippage: {trade.get('buy_slippage', 'not configured') * 100}%")
print(f" - Extreme fast mode: {'enabled' if trade.get('extreme_fast_mode') else 'disabled'}")
fees = config.get('priority_fees', {})
print("Priority fees:")
if fees.get('enable_dynamic'):
print(" - Dynamic fees enabled")
elif fees.get('enable_fixed'):
print(f" - Fixed fee: {fees.get('fixed_amount', 'not configured')} microlamports")
print("Configuration loaded successfully!")
if __name__ == "__main__":
config = load_bot_config("bots/bot-sniper.yaml")
print_config_summary(config)
View File
+240
View File
@@ -0,0 +1,240 @@
"""
Solana client abstraction for blockchain operations.
"""
import asyncio
import json
from typing import Any
import aiohttp
from solana.rpc.async_api import AsyncClient
from solana.rpc.commitment import Processed
from solana.rpc.types import TxOpts
from solders.compute_budget import set_compute_unit_limit, set_compute_unit_price
from solders.hash import Hash
from solders.instruction import Instruction
from solders.keypair import Keypair
from solders.message import Message
from solders.pubkey import Pubkey
from solders.transaction import Transaction
from utils.logger import get_logger
logger = get_logger(__name__)
class SolanaClient:
"""Abstraction for Solana RPC client operations."""
def __init__(self, rpc_endpoint: str):
"""Initialize Solana client with RPC endpoint.
Args:
rpc_endpoint: URL of the Solana RPC endpoint
"""
self.rpc_endpoint = rpc_endpoint
self._client = None
self._cached_blockhash: Hash | None = None
self._blockhash_lock = asyncio.Lock()
self._blockhash_updater_task = asyncio.create_task(self.start_blockhash_updater())
async def start_blockhash_updater(self, interval: float = 5.0):
"""Start background task to update recent blockhash."""
while True:
try:
blockhash = await self.get_latest_blockhash()
async with self._blockhash_lock:
self._cached_blockhash = blockhash
except Exception as e:
logger.warning(f"Blockhash fetch failed: {e!s}")
finally:
await asyncio.sleep(interval)
async def get_cached_blockhash(self) -> Hash:
"""Return the most recently cached blockhash."""
async with self._blockhash_lock:
if self._cached_blockhash is None:
raise RuntimeError("No cached blockhash available yet")
return self._cached_blockhash
async def get_client(self) -> AsyncClient:
"""Get or create the AsyncClient instance.
Returns:
AsyncClient instance
"""
if self._client is None:
self._client = AsyncClient(self.rpc_endpoint)
return self._client
async def close(self):
"""Close the client connection and stop the blockhash updater."""
if self._blockhash_updater_task:
self._blockhash_updater_task.cancel()
try:
await self._blockhash_updater_task
except asyncio.CancelledError:
pass
if self._client:
await self._client.close()
self._client = None
async def get_health(self) -> str | None:
body = {
"jsonrpc": "2.0",
"id": 1,
"method": "getHealth",
}
result = await self.post_rpc(body)
if result and "result" in result:
return result["result"]
return None
async def get_account_info(self, pubkey: Pubkey) -> dict[str, Any]:
"""Get account info from the blockchain.
Args:
pubkey: Public key of the account
Returns:
Account info response
Raises:
ValueError: If account doesn't exist or has no data
"""
client = await self.get_client()
response = await client.get_account_info(pubkey, encoding="base64") # base64 encoding for account data by default
if not response.value:
raise ValueError(f"Account {pubkey} not found")
return response.value
async def get_token_account_balance(self, token_account: Pubkey) -> int:
"""Get token balance for an account.
Args:
token_account: Token account address
Returns:
Token balance as integer
"""
client = await self.get_client()
response = await client.get_token_account_balance(token_account)
if response.value:
return int(response.value.amount)
return 0
async def get_latest_blockhash(self) -> Hash:
"""Get the latest blockhash.
Returns:
Recent blockhash as string
"""
client = await self.get_client()
response = await client.get_latest_blockhash(commitment="processed")
return response.value.blockhash
async def build_and_send_transaction(
self,
instructions: list[Instruction],
signer_keypair: Keypair,
skip_preflight: bool = True,
max_retries: int = 3,
priority_fee: int | None = None,
) -> str:
"""
Send a transaction with optional priority fee.
Args:
instructions: List of instructions to include in the transaction.
skip_preflight: Whether to skip preflight checks.
max_retries: Maximum number of retry attempts.
priority_fee: Optional priority fee in microlamports.
Returns:
Transaction signature.
"""
client = await self.get_client()
logger.info(
f"Priority fee in microlamports: {priority_fee if priority_fee else 0}"
)
# Add priority fee instructions if applicable
if priority_fee is not None:
fee_instructions = [
set_compute_unit_limit(72_000), # Default compute unit limit
set_compute_unit_price(priority_fee),
]
instructions = fee_instructions + instructions
recent_blockhash = await self.get_cached_blockhash()
message = Message(instructions, signer_keypair.pubkey())
transaction = Transaction([signer_keypair], message, recent_blockhash)
for attempt in range(max_retries):
try:
tx_opts = TxOpts(
skip_preflight=skip_preflight, preflight_commitment=Processed
)
response = await client.send_transaction(transaction, tx_opts)
return response.value
except Exception as e:
if attempt == max_retries - 1:
logger.error(
f"Failed to send transaction after {max_retries} attempts"
)
raise
wait_time = 2**attempt
logger.warning(
f"Transaction attempt {attempt + 1} failed: {e!s}, retrying in {wait_time}s"
)
await asyncio.sleep(wait_time)
async def confirm_transaction(
self, signature: str, commitment: str = "confirmed"
) -> bool:
"""Wait for transaction confirmation.
Args:
signature: Transaction signature
commitment: Confirmation commitment level
Returns:
Whether transaction was confirmed
"""
client = await self.get_client()
try:
await client.confirm_transaction(signature, commitment=commitment, sleep_seconds=1)
return True
except Exception as e:
logger.error(f"Failed to confirm transaction {signature}: {e!s}")
return False
async def post_rpc(self, body: dict[str, Any]) -> dict[str, Any] | None:
"""
Send a raw RPC request to the Solana node.
Args:
body: JSON-RPC request body.
Returns:
Optional[Dict[str, Any]]: Parsed JSON response, or None if the request fails.
"""
try:
async with aiohttp.ClientSession() as session:
async with session.post(
self.rpc_endpoint,
json=body,
timeout=aiohttp.ClientTimeout(10), # 10-second timeout
) as response:
response.raise_for_status()
return await response.json()
except aiohttp.ClientError as e:
logger.error(f"RPC request failed: {e!s}", exc_info=True)
return None
except json.JSONDecodeError as e:
logger.error(f"Failed to decode RPC response: {e!s}", exc_info=True)
return None
+135
View File
@@ -0,0 +1,135 @@
"""
Bonding curve operations for pump.fun tokens.
"""
import struct
from typing import Final
from construct import Flag, Int64ul, Struct
from solders.pubkey import Pubkey
from core.client import SolanaClient
from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS
from utils.logger import get_logger
logger = get_logger(__name__)
# Discriminator for the bonding curve account
EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack("<Q", 6966180631402821399)
class BondingCurveState:
"""Represents the state of a pump.fun bonding curve."""
_STRUCT = Struct(
"virtual_token_reserves" / Int64ul,
"virtual_sol_reserves" / Int64ul,
"real_token_reserves" / Int64ul,
"real_sol_reserves" / Int64ul,
"token_total_supply" / Int64ul,
"complete" / Flag,
)
def __init__(self, data: bytes) -> None:
"""Parse bonding curve data.
Args:
data: Raw account data
Raises:
ValueError: If data cannot be parsed
"""
if data[:8] != EXPECTED_DISCRIMINATOR:
raise ValueError("Invalid curve state discriminator")
parsed = self._STRUCT.parse(data[8:])
self.__dict__.update(parsed)
def calculate_price(self) -> float:
"""Calculate token price in SOL.
Returns:
Token price in SOL
Raises:
ValueError: If reserve state is invalid
"""
if self.virtual_token_reserves <= 0 or self.virtual_sol_reserves <= 0:
raise ValueError("Invalid reserve state")
return (self.virtual_sol_reserves / LAMPORTS_PER_SOL) / (
self.virtual_token_reserves / 10**TOKEN_DECIMALS
)
@property
def token_reserves(self) -> float:
"""Get token reserves in decimal form."""
return self.virtual_token_reserves / 10**TOKEN_DECIMALS
@property
def sol_reserves(self) -> float:
"""Get SOL reserves in decimal form."""
return self.virtual_sol_reserves / LAMPORTS_PER_SOL
class BondingCurveManager:
"""Manager for bonding curve operations."""
def __init__(self, client: SolanaClient):
"""Initialize with Solana client.
Args:
client: Solana client for RPC calls
"""
self.client = client
async def get_curve_state(self, curve_address: Pubkey) -> BondingCurveState:
"""Get the state of a bonding curve.
Args:
curve_address: Address of the bonding curve account
Returns:
Bonding curve state
Raises:
ValueError: If curve data is invalid
"""
try:
account = await self.client.get_account_info(curve_address)
if not account.data:
raise ValueError(f"No data in bonding curve account {curve_address}")
return BondingCurveState(account.data)
except Exception as e:
logger.error(f"Failed to get curve state: {str(e)}")
raise ValueError(f"Invalid curve state: {str(e)}")
async def calculate_price(self, curve_address: Pubkey) -> float:
"""Calculate the current price of a token.
Args:
curve_address: Address of the bonding curve account
Returns:
Token price in SOL
"""
curve_state = await self.get_curve_state(curve_address)
return curve_state.calculate_price()
async def calculate_expected_tokens(
self, curve_address: Pubkey, sol_amount: float
) -> float:
"""Calculate the expected token amount for a given SOL input.
Args:
curve_address: Address of the bonding curve account
sol_amount: Amount of SOL to spend
Returns:
Expected token amount
"""
curve_state = await self.get_curve_state(curve_address)
price = curve_state.calculate_price()
return sol_amount / price
+15
View File
@@ -0,0 +1,15 @@
from abc import ABC, abstractmethod
class PriorityFeePlugin(ABC):
"""Base class for priority fee calculation plugins."""
@abstractmethod
async def get_priority_fee(self) -> int | None:
"""
Calculate the priority fee.
Returns:
Optional[int]: Priority fee in lamports, or None if no fee should be applied.
"""
pass
+69
View File
@@ -0,0 +1,69 @@
import statistics
from solders.pubkey import Pubkey
from core.client import SolanaClient
from core.priority_fee import PriorityFeePlugin
from utils.logger import get_logger
logger = get_logger(__name__)
class DynamicPriorityFee(PriorityFeePlugin):
"""Dynamic priority fee plugin using getRecentPrioritizationFees."""
def __init__(self, client: SolanaClient):
"""
Initialize the dynamic fee plugin.
Args:
client: Solana RPC client for network requests.
"""
self.client = client
async def get_priority_fee(
self, accounts: list[Pubkey] | None = None
) -> int | None:
"""
Fetch the recent priority fee using getRecentPrioritizationFees.
Args:
accounts: List of accounts to consider for the fee calculation.
If None, the fee is calculated without specific account constraints.
Returns:
Optional[int]: Median priority fee in microlamports, or None if the request fails.
"""
try:
body = {
"jsonrpc": "2.0",
"id": 1,
"method": "getRecentPrioritizationFees",
"params": [[str(account) for account in accounts]] if accounts else [],
}
response = await self.client.post_rpc(body)
if not response or "result" not in response:
logger.error(
"Failed to fetch recent prioritization fees: invalid response"
)
return None
fees = [fee["prioritizationFee"] for fee in response["result"]]
if not fees:
logger.warning("No prioritization fees found in the response")
return None
# Get the 70th percentile of fees for faster processing
# It means you're paying a fee that's higher than 70% of other transactions
# Higher percentile = faster transactions but more expensive
# Lower percentile = cheaper but slower transactions
prior_fee = int(statistics.quantiles(fees, n=10)[-3]) # 70th percentile
return prior_fee
except Exception as e:
logger.error(
f"Failed to fetch recent priority fee: {str(e)}", exc_info=True
)
return None
+25
View File
@@ -0,0 +1,25 @@
from . import PriorityFeePlugin
class FixedPriorityFee(PriorityFeePlugin):
"""Fixed priority fee plugin."""
def __init__(self, fixed_fee: int):
"""
Initialize the fixed fee plugin.
Args:
fixed_fee: Fixed priority fee in microlamports.
"""
self.fixed_fee = fixed_fee
async def get_priority_fee(self) -> int | None:
"""
Return the fixed priority fee.
Returns:
Optional[int]: Fixed priority fee in microlamports, or None if fixed_fee is 0.
"""
if self.fixed_fee == 0:
return None
return self.fixed_fee
+92
View File
@@ -0,0 +1,92 @@
from solders.pubkey import Pubkey
from core.client import SolanaClient
from core.priority_fee.dynamic_fee import DynamicPriorityFee
from core.priority_fee.fixed_fee import FixedPriorityFee
from utils.logger import get_logger
logger = get_logger(__name__)
class PriorityFeeManager:
"""Manager for priority fee calculation and validation."""
def __init__(
self,
client: SolanaClient,
enable_dynamic_fee: bool,
enable_fixed_fee: bool,
fixed_fee: int,
extra_fee: float,
hard_cap: int,
):
"""
Initialize the priority fee manager.
Args:
client: Solana RPC client for dynamic fee calculation.
enable_dynamic_fee: Whether to enable dynamic fee calculation.
enable_fixed_fee: Whether to enable fixed fee.
fixed_fee: Fixed priority fee in microlamports.
extra_fee: Percentage increase to apply to the base fee.
hard_cap: Maximum allowed priority fee in microlamports.
"""
self.client = client
self.enable_dynamic_fee = enable_dynamic_fee
self.enable_fixed_fee = enable_fixed_fee
self.fixed_fee = fixed_fee
self.extra_fee = extra_fee
self.hard_cap = hard_cap
# Initialize plugins
self.dynamic_fee_plugin = DynamicPriorityFee(client)
self.fixed_fee_plugin = FixedPriorityFee(fixed_fee)
async def calculate_priority_fee(
self, accounts: list[Pubkey] | None = None
) -> int | None:
"""
Calculate the priority fee based on the configuration.
Args:
accounts: List of accounts to consider for dynamic fee calculation.
If None, the fee is calculated without specific account constraints.
Returns:
Optional[int]: Calculated priority fee in microlamports, or None if no fee should be applied.
"""
base_fee = await self._get_base_fee(accounts)
if base_fee is None:
return None
# Apply extra fee (percentage increase)
final_fee = int(base_fee * (1 + self.extra_fee))
# Enforce hard cap
if final_fee > self.hard_cap:
logger.warning(
f"Calculated priority fee {final_fee} exceeds hard cap {self.hard_cap}. Applying hard cap."
)
final_fee = self.hard_cap
return final_fee
async def _get_base_fee(self, accounts: list[Pubkey] | None = None) -> int | None:
"""
Determine the base fee based on the configuration.
Returns:
Optional[int]: Base fee in microlamports, or None if no fee should be applied.
"""
# Prefer dynamic fee if both are enabled
if self.enable_dynamic_fee:
dynamic_fee = await self.dynamic_fee_plugin.get_priority_fee(accounts)
if dynamic_fee is not None:
return dynamic_fee
# Fall back to fixed fee if enabled
if self.enable_fixed_fee:
return await self.fixed_fee_plugin.get_priority_fee()
# No priority fee if both are disabled
return None
+51
View File
@@ -0,0 +1,51 @@
"""
System and program addresses for Solana and pump.fun interactions.
"""
from dataclasses import dataclass
from typing import Final
from solders.pubkey import Pubkey
LAMPORTS_PER_SOL: Final[int] = 1_000_000_000
TOKEN_DECIMALS: Final[int] = 6
@dataclass
class SystemAddresses:
"""System-level Solana addresses."""
PROGRAM: Final[Pubkey] = Pubkey.from_string("11111111111111111111111111111111")
TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string(
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
)
ASSOCIATED_TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
)
RENT: Final[Pubkey] = Pubkey.from_string(
"SysvarRent111111111111111111111111111111111"
)
SOL: Final[Pubkey] = Pubkey.from_string(
"So11111111111111111111111111111111111111112"
)
@dataclass
class PumpAddresses:
"""Pump.fun program addresses."""
PROGRAM: Final[Pubkey] = Pubkey.from_string(
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
)
GLOBAL: Final[Pubkey] = Pubkey.from_string(
"4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf"
)
EVENT_AUTHORITY: Final[Pubkey] = Pubkey.from_string(
"Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1"
)
FEE: Final[Pubkey] = Pubkey.from_string(
"CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM"
)
LIQUIDITY_MIGRATOR: Final[Pubkey] = Pubkey.from_string(
"39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg"
)
+55
View File
@@ -0,0 +1,55 @@
"""
Wallet management for Solana transactions.
"""
import base58
from solders.keypair import Keypair
from solders.pubkey import Pubkey
from spl.token.instructions import get_associated_token_address
class Wallet:
"""Manages a Solana wallet for trading operations."""
def __init__(self, private_key: str):
"""Initialize wallet from private key.
Args:
private_key: Base58 encoded private key
"""
self._private_key = private_key
self._keypair = self._load_keypair(private_key)
@property
def pubkey(self) -> Pubkey:
"""Get the public key of the wallet."""
return self._keypair.pubkey()
@property
def keypair(self) -> Keypair:
"""Get the keypair for signing transactions."""
return self._keypair
def get_associated_token_address(self, mint: Pubkey) -> Pubkey:
"""Get the associated token account address for a mint.
Args:
mint: Token mint address
Returns:
Associated token account address
"""
return get_associated_token_address(self.pubkey, mint)
@staticmethod
def _load_keypair(private_key: str) -> Keypair:
"""Load keypair from private key.
Args:
private_key: Base58 encoded private key
Returns:
Solana keypair
"""
private_key_bytes = base58.b58decode(private_key)
return Keypair.from_bytes(private_key_bytes)
File diff suppressed because one or more lines are too long
+485
View File
@@ -0,0 +1,485 @@
import solana_storage_pb2 as _solana_storage_pb2
from google.protobuf.internal import containers as _containers
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
from solana_storage_pb2 import ConfirmedBlock as ConfirmedBlock
from solana_storage_pb2 import ConfirmedTransaction as ConfirmedTransaction
from solana_storage_pb2 import Transaction as Transaction
from solana_storage_pb2 import Message as Message
from solana_storage_pb2 import MessageHeader as MessageHeader
from solana_storage_pb2 import MessageAddressTableLookup as MessageAddressTableLookup
from solana_storage_pb2 import TransactionStatusMeta as TransactionStatusMeta
from solana_storage_pb2 import TransactionError as TransactionError
from solana_storage_pb2 import InnerInstructions as InnerInstructions
from solana_storage_pb2 import InnerInstruction as InnerInstruction
from solana_storage_pb2 import CompiledInstruction as CompiledInstruction
from solana_storage_pb2 import TokenBalance as TokenBalance
from solana_storage_pb2 import UiTokenAmount as UiTokenAmount
from solana_storage_pb2 import ReturnData as ReturnData
from solana_storage_pb2 import Reward as Reward
from solana_storage_pb2 import Rewards as Rewards
from solana_storage_pb2 import UnixTimestamp as UnixTimestamp
from solana_storage_pb2 import BlockHeight as BlockHeight
from solana_storage_pb2 import NumPartitions as NumPartitions
from solana_storage_pb2 import RewardType as RewardType
DESCRIPTOR: _descriptor.FileDescriptor
Unspecified: _solana_storage_pb2.RewardType
Fee: _solana_storage_pb2.RewardType
Rent: _solana_storage_pb2.RewardType
Staking: _solana_storage_pb2.RewardType
Voting: _solana_storage_pb2.RewardType
class CommitmentLevel(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
PROCESSED: _ClassVar[CommitmentLevel]
CONFIRMED: _ClassVar[CommitmentLevel]
FINALIZED: _ClassVar[CommitmentLevel]
FIRST_SHRED_RECEIVED: _ClassVar[CommitmentLevel]
COMPLETED: _ClassVar[CommitmentLevel]
CREATED_BANK: _ClassVar[CommitmentLevel]
DEAD: _ClassVar[CommitmentLevel]
PROCESSED: CommitmentLevel
CONFIRMED: CommitmentLevel
FINALIZED: CommitmentLevel
FIRST_SHRED_RECEIVED: CommitmentLevel
COMPLETED: CommitmentLevel
CREATED_BANK: CommitmentLevel
DEAD: CommitmentLevel
class SubscribeRequest(_message.Message):
__slots__ = ("accounts", "slots", "transactions", "transactions_status", "blocks", "blocks_meta", "entry", "commitment", "accounts_data_slice", "ping")
class AccountsEntry(_message.Message):
__slots__ = ("key", "value")
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: SubscribeRequestFilterAccounts
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterAccounts, _Mapping]] = ...) -> None: ...
class SlotsEntry(_message.Message):
__slots__ = ("key", "value")
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: SubscribeRequestFilterSlots
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterSlots, _Mapping]] = ...) -> None: ...
class TransactionsEntry(_message.Message):
__slots__ = ("key", "value")
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: SubscribeRequestFilterTransactions
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterTransactions, _Mapping]] = ...) -> None: ...
class TransactionsStatusEntry(_message.Message):
__slots__ = ("key", "value")
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: SubscribeRequestFilterTransactions
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterTransactions, _Mapping]] = ...) -> None: ...
class BlocksEntry(_message.Message):
__slots__ = ("key", "value")
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: SubscribeRequestFilterBlocks
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterBlocks, _Mapping]] = ...) -> None: ...
class BlocksMetaEntry(_message.Message):
__slots__ = ("key", "value")
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: SubscribeRequestFilterBlocksMeta
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterBlocksMeta, _Mapping]] = ...) -> None: ...
class EntryEntry(_message.Message):
__slots__ = ("key", "value")
KEY_FIELD_NUMBER: _ClassVar[int]
VALUE_FIELD_NUMBER: _ClassVar[int]
key: str
value: SubscribeRequestFilterEntry
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterEntry, _Mapping]] = ...) -> None: ...
ACCOUNTS_FIELD_NUMBER: _ClassVar[int]
SLOTS_FIELD_NUMBER: _ClassVar[int]
TRANSACTIONS_FIELD_NUMBER: _ClassVar[int]
TRANSACTIONS_STATUS_FIELD_NUMBER: _ClassVar[int]
BLOCKS_FIELD_NUMBER: _ClassVar[int]
BLOCKS_META_FIELD_NUMBER: _ClassVar[int]
ENTRY_FIELD_NUMBER: _ClassVar[int]
COMMITMENT_FIELD_NUMBER: _ClassVar[int]
ACCOUNTS_DATA_SLICE_FIELD_NUMBER: _ClassVar[int]
PING_FIELD_NUMBER: _ClassVar[int]
accounts: _containers.MessageMap[str, SubscribeRequestFilterAccounts]
slots: _containers.MessageMap[str, SubscribeRequestFilterSlots]
transactions: _containers.MessageMap[str, SubscribeRequestFilterTransactions]
transactions_status: _containers.MessageMap[str, SubscribeRequestFilterTransactions]
blocks: _containers.MessageMap[str, SubscribeRequestFilterBlocks]
blocks_meta: _containers.MessageMap[str, SubscribeRequestFilterBlocksMeta]
entry: _containers.MessageMap[str, SubscribeRequestFilterEntry]
commitment: CommitmentLevel
accounts_data_slice: _containers.RepeatedCompositeFieldContainer[SubscribeRequestAccountsDataSlice]
ping: SubscribeRequestPing
def __init__(self, accounts: _Optional[_Mapping[str, SubscribeRequestFilterAccounts]] = ..., slots: _Optional[_Mapping[str, SubscribeRequestFilterSlots]] = ..., transactions: _Optional[_Mapping[str, SubscribeRequestFilterTransactions]] = ..., transactions_status: _Optional[_Mapping[str, SubscribeRequestFilterTransactions]] = ..., blocks: _Optional[_Mapping[str, SubscribeRequestFilterBlocks]] = ..., blocks_meta: _Optional[_Mapping[str, SubscribeRequestFilterBlocksMeta]] = ..., entry: _Optional[_Mapping[str, SubscribeRequestFilterEntry]] = ..., commitment: _Optional[_Union[CommitmentLevel, str]] = ..., accounts_data_slice: _Optional[_Iterable[_Union[SubscribeRequestAccountsDataSlice, _Mapping]]] = ..., ping: _Optional[_Union[SubscribeRequestPing, _Mapping]] = ...) -> None: ...
class SubscribeRequestFilterAccounts(_message.Message):
__slots__ = ("account", "owner", "filters", "nonempty_txn_signature")
ACCOUNT_FIELD_NUMBER: _ClassVar[int]
OWNER_FIELD_NUMBER: _ClassVar[int]
FILTERS_FIELD_NUMBER: _ClassVar[int]
NONEMPTY_TXN_SIGNATURE_FIELD_NUMBER: _ClassVar[int]
account: _containers.RepeatedScalarFieldContainer[str]
owner: _containers.RepeatedScalarFieldContainer[str]
filters: _containers.RepeatedCompositeFieldContainer[SubscribeRequestFilterAccountsFilter]
nonempty_txn_signature: bool
def __init__(self, account: _Optional[_Iterable[str]] = ..., owner: _Optional[_Iterable[str]] = ..., filters: _Optional[_Iterable[_Union[SubscribeRequestFilterAccountsFilter, _Mapping]]] = ..., nonempty_txn_signature: bool = ...) -> None: ...
class SubscribeRequestFilterAccountsFilter(_message.Message):
__slots__ = ("memcmp", "datasize", "token_account_state", "lamports")
MEMCMP_FIELD_NUMBER: _ClassVar[int]
DATASIZE_FIELD_NUMBER: _ClassVar[int]
TOKEN_ACCOUNT_STATE_FIELD_NUMBER: _ClassVar[int]
LAMPORTS_FIELD_NUMBER: _ClassVar[int]
memcmp: SubscribeRequestFilterAccountsFilterMemcmp
datasize: int
token_account_state: bool
lamports: SubscribeRequestFilterAccountsFilterLamports
def __init__(self, memcmp: _Optional[_Union[SubscribeRequestFilterAccountsFilterMemcmp, _Mapping]] = ..., datasize: _Optional[int] = ..., token_account_state: bool = ..., lamports: _Optional[_Union[SubscribeRequestFilterAccountsFilterLamports, _Mapping]] = ...) -> None: ...
class SubscribeRequestFilterAccountsFilterMemcmp(_message.Message):
__slots__ = ("offset", "bytes", "base58", "base64")
OFFSET_FIELD_NUMBER: _ClassVar[int]
BYTES_FIELD_NUMBER: _ClassVar[int]
BASE58_FIELD_NUMBER: _ClassVar[int]
BASE64_FIELD_NUMBER: _ClassVar[int]
offset: int
bytes: bytes
base58: str
base64: str
def __init__(self, offset: _Optional[int] = ..., bytes: _Optional[bytes] = ..., base58: _Optional[str] = ..., base64: _Optional[str] = ...) -> None: ...
class SubscribeRequestFilterAccountsFilterLamports(_message.Message):
__slots__ = ("eq", "ne", "lt", "gt")
EQ_FIELD_NUMBER: _ClassVar[int]
NE_FIELD_NUMBER: _ClassVar[int]
LT_FIELD_NUMBER: _ClassVar[int]
GT_FIELD_NUMBER: _ClassVar[int]
eq: int
ne: int
lt: int
gt: int
def __init__(self, eq: _Optional[int] = ..., ne: _Optional[int] = ..., lt: _Optional[int] = ..., gt: _Optional[int] = ...) -> None: ...
class SubscribeRequestFilterSlots(_message.Message):
__slots__ = ("filter_by_commitment",)
FILTER_BY_COMMITMENT_FIELD_NUMBER: _ClassVar[int]
filter_by_commitment: bool
def __init__(self, filter_by_commitment: bool = ...) -> None: ...
class SubscribeRequestFilterTransactions(_message.Message):
__slots__ = ("vote", "failed", "signature", "account_include", "account_exclude", "account_required")
VOTE_FIELD_NUMBER: _ClassVar[int]
FAILED_FIELD_NUMBER: _ClassVar[int]
SIGNATURE_FIELD_NUMBER: _ClassVar[int]
ACCOUNT_INCLUDE_FIELD_NUMBER: _ClassVar[int]
ACCOUNT_EXCLUDE_FIELD_NUMBER: _ClassVar[int]
ACCOUNT_REQUIRED_FIELD_NUMBER: _ClassVar[int]
vote: bool
failed: bool
signature: str
account_include: _containers.RepeatedScalarFieldContainer[str]
account_exclude: _containers.RepeatedScalarFieldContainer[str]
account_required: _containers.RepeatedScalarFieldContainer[str]
def __init__(self, vote: bool = ..., failed: bool = ..., signature: _Optional[str] = ..., account_include: _Optional[_Iterable[str]] = ..., account_exclude: _Optional[_Iterable[str]] = ..., account_required: _Optional[_Iterable[str]] = ...) -> None: ...
class SubscribeRequestFilterBlocks(_message.Message):
__slots__ = ("account_include", "include_transactions", "include_accounts", "include_entries")
ACCOUNT_INCLUDE_FIELD_NUMBER: _ClassVar[int]
INCLUDE_TRANSACTIONS_FIELD_NUMBER: _ClassVar[int]
INCLUDE_ACCOUNTS_FIELD_NUMBER: _ClassVar[int]
INCLUDE_ENTRIES_FIELD_NUMBER: _ClassVar[int]
account_include: _containers.RepeatedScalarFieldContainer[str]
include_transactions: bool
include_accounts: bool
include_entries: bool
def __init__(self, account_include: _Optional[_Iterable[str]] = ..., include_transactions: bool = ..., include_accounts: bool = ..., include_entries: bool = ...) -> None: ...
class SubscribeRequestFilterBlocksMeta(_message.Message):
__slots__ = ()
def __init__(self) -> None: ...
class SubscribeRequestFilterEntry(_message.Message):
__slots__ = ()
def __init__(self) -> None: ...
class SubscribeRequestAccountsDataSlice(_message.Message):
__slots__ = ("offset", "length")
OFFSET_FIELD_NUMBER: _ClassVar[int]
LENGTH_FIELD_NUMBER: _ClassVar[int]
offset: int
length: int
def __init__(self, offset: _Optional[int] = ..., length: _Optional[int] = ...) -> None: ...
class SubscribeRequestPing(_message.Message):
__slots__ = ("id",)
ID_FIELD_NUMBER: _ClassVar[int]
id: int
def __init__(self, id: _Optional[int] = ...) -> None: ...
class SubscribeUpdate(_message.Message):
__slots__ = ("filters", "account", "slot", "transaction", "transaction_status", "block", "ping", "pong", "block_meta", "entry")
FILTERS_FIELD_NUMBER: _ClassVar[int]
ACCOUNT_FIELD_NUMBER: _ClassVar[int]
SLOT_FIELD_NUMBER: _ClassVar[int]
TRANSACTION_FIELD_NUMBER: _ClassVar[int]
TRANSACTION_STATUS_FIELD_NUMBER: _ClassVar[int]
BLOCK_FIELD_NUMBER: _ClassVar[int]
PING_FIELD_NUMBER: _ClassVar[int]
PONG_FIELD_NUMBER: _ClassVar[int]
BLOCK_META_FIELD_NUMBER: _ClassVar[int]
ENTRY_FIELD_NUMBER: _ClassVar[int]
filters: _containers.RepeatedScalarFieldContainer[str]
account: SubscribeUpdateAccount
slot: SubscribeUpdateSlot
transaction: SubscribeUpdateTransaction
transaction_status: SubscribeUpdateTransactionStatus
block: SubscribeUpdateBlock
ping: SubscribeUpdatePing
pong: SubscribeUpdatePong
block_meta: SubscribeUpdateBlockMeta
entry: SubscribeUpdateEntry
def __init__(self, filters: _Optional[_Iterable[str]] = ..., account: _Optional[_Union[SubscribeUpdateAccount, _Mapping]] = ..., slot: _Optional[_Union[SubscribeUpdateSlot, _Mapping]] = ..., transaction: _Optional[_Union[SubscribeUpdateTransaction, _Mapping]] = ..., transaction_status: _Optional[_Union[SubscribeUpdateTransactionStatus, _Mapping]] = ..., block: _Optional[_Union[SubscribeUpdateBlock, _Mapping]] = ..., ping: _Optional[_Union[SubscribeUpdatePing, _Mapping]] = ..., pong: _Optional[_Union[SubscribeUpdatePong, _Mapping]] = ..., block_meta: _Optional[_Union[SubscribeUpdateBlockMeta, _Mapping]] = ..., entry: _Optional[_Union[SubscribeUpdateEntry, _Mapping]] = ...) -> None: ...
class SubscribeUpdateAccount(_message.Message):
__slots__ = ("account", "slot", "is_startup")
ACCOUNT_FIELD_NUMBER: _ClassVar[int]
SLOT_FIELD_NUMBER: _ClassVar[int]
IS_STARTUP_FIELD_NUMBER: _ClassVar[int]
account: SubscribeUpdateAccountInfo
slot: int
is_startup: bool
def __init__(self, account: _Optional[_Union[SubscribeUpdateAccountInfo, _Mapping]] = ..., slot: _Optional[int] = ..., is_startup: bool = ...) -> None: ...
class SubscribeUpdateAccountInfo(_message.Message):
__slots__ = ("pubkey", "lamports", "owner", "executable", "rent_epoch", "data", "write_version", "txn_signature")
PUBKEY_FIELD_NUMBER: _ClassVar[int]
LAMPORTS_FIELD_NUMBER: _ClassVar[int]
OWNER_FIELD_NUMBER: _ClassVar[int]
EXECUTABLE_FIELD_NUMBER: _ClassVar[int]
RENT_EPOCH_FIELD_NUMBER: _ClassVar[int]
DATA_FIELD_NUMBER: _ClassVar[int]
WRITE_VERSION_FIELD_NUMBER: _ClassVar[int]
TXN_SIGNATURE_FIELD_NUMBER: _ClassVar[int]
pubkey: bytes
lamports: int
owner: bytes
executable: bool
rent_epoch: int
data: bytes
write_version: int
txn_signature: bytes
def __init__(self, pubkey: _Optional[bytes] = ..., lamports: _Optional[int] = ..., owner: _Optional[bytes] = ..., executable: bool = ..., rent_epoch: _Optional[int] = ..., data: _Optional[bytes] = ..., write_version: _Optional[int] = ..., txn_signature: _Optional[bytes] = ...) -> None: ...
class SubscribeUpdateSlot(_message.Message):
__slots__ = ("slot", "parent", "status", "dead_error")
SLOT_FIELD_NUMBER: _ClassVar[int]
PARENT_FIELD_NUMBER: _ClassVar[int]
STATUS_FIELD_NUMBER: _ClassVar[int]
DEAD_ERROR_FIELD_NUMBER: _ClassVar[int]
slot: int
parent: int
status: CommitmentLevel
dead_error: str
def __init__(self, slot: _Optional[int] = ..., parent: _Optional[int] = ..., status: _Optional[_Union[CommitmentLevel, str]] = ..., dead_error: _Optional[str] = ...) -> None: ...
class SubscribeUpdateTransaction(_message.Message):
__slots__ = ("transaction", "slot")
TRANSACTION_FIELD_NUMBER: _ClassVar[int]
SLOT_FIELD_NUMBER: _ClassVar[int]
transaction: SubscribeUpdateTransactionInfo
slot: int
def __init__(self, transaction: _Optional[_Union[SubscribeUpdateTransactionInfo, _Mapping]] = ..., slot: _Optional[int] = ...) -> None: ...
class SubscribeUpdateTransactionInfo(_message.Message):
__slots__ = ("signature", "is_vote", "transaction", "meta", "index")
SIGNATURE_FIELD_NUMBER: _ClassVar[int]
IS_VOTE_FIELD_NUMBER: _ClassVar[int]
TRANSACTION_FIELD_NUMBER: _ClassVar[int]
META_FIELD_NUMBER: _ClassVar[int]
INDEX_FIELD_NUMBER: _ClassVar[int]
signature: bytes
is_vote: bool
transaction: _solana_storage_pb2.Transaction
meta: _solana_storage_pb2.TransactionStatusMeta
index: int
def __init__(self, signature: _Optional[bytes] = ..., is_vote: bool = ..., transaction: _Optional[_Union[_solana_storage_pb2.Transaction, _Mapping]] = ..., meta: _Optional[_Union[_solana_storage_pb2.TransactionStatusMeta, _Mapping]] = ..., index: _Optional[int] = ...) -> None: ...
class SubscribeUpdateTransactionStatus(_message.Message):
__slots__ = ("slot", "signature", "is_vote", "index", "err")
SLOT_FIELD_NUMBER: _ClassVar[int]
SIGNATURE_FIELD_NUMBER: _ClassVar[int]
IS_VOTE_FIELD_NUMBER: _ClassVar[int]
INDEX_FIELD_NUMBER: _ClassVar[int]
ERR_FIELD_NUMBER: _ClassVar[int]
slot: int
signature: bytes
is_vote: bool
index: int
err: _solana_storage_pb2.TransactionError
def __init__(self, slot: _Optional[int] = ..., signature: _Optional[bytes] = ..., is_vote: bool = ..., index: _Optional[int] = ..., err: _Optional[_Union[_solana_storage_pb2.TransactionError, _Mapping]] = ...) -> None: ...
class SubscribeUpdateBlock(_message.Message):
__slots__ = ("slot", "blockhash", "rewards", "block_time", "block_height", "parent_slot", "parent_blockhash", "executed_transaction_count", "transactions", "updated_account_count", "accounts", "entries_count", "entries")
SLOT_FIELD_NUMBER: _ClassVar[int]
BLOCKHASH_FIELD_NUMBER: _ClassVar[int]
REWARDS_FIELD_NUMBER: _ClassVar[int]
BLOCK_TIME_FIELD_NUMBER: _ClassVar[int]
BLOCK_HEIGHT_FIELD_NUMBER: _ClassVar[int]
PARENT_SLOT_FIELD_NUMBER: _ClassVar[int]
PARENT_BLOCKHASH_FIELD_NUMBER: _ClassVar[int]
EXECUTED_TRANSACTION_COUNT_FIELD_NUMBER: _ClassVar[int]
TRANSACTIONS_FIELD_NUMBER: _ClassVar[int]
UPDATED_ACCOUNT_COUNT_FIELD_NUMBER: _ClassVar[int]
ACCOUNTS_FIELD_NUMBER: _ClassVar[int]
ENTRIES_COUNT_FIELD_NUMBER: _ClassVar[int]
ENTRIES_FIELD_NUMBER: _ClassVar[int]
slot: int
blockhash: str
rewards: _solana_storage_pb2.Rewards
block_time: _solana_storage_pb2.UnixTimestamp
block_height: _solana_storage_pb2.BlockHeight
parent_slot: int
parent_blockhash: str
executed_transaction_count: int
transactions: _containers.RepeatedCompositeFieldContainer[SubscribeUpdateTransactionInfo]
updated_account_count: int
accounts: _containers.RepeatedCompositeFieldContainer[SubscribeUpdateAccountInfo]
entries_count: int
entries: _containers.RepeatedCompositeFieldContainer[SubscribeUpdateEntry]
def __init__(self, slot: _Optional[int] = ..., blockhash: _Optional[str] = ..., rewards: _Optional[_Union[_solana_storage_pb2.Rewards, _Mapping]] = ..., block_time: _Optional[_Union[_solana_storage_pb2.UnixTimestamp, _Mapping]] = ..., block_height: _Optional[_Union[_solana_storage_pb2.BlockHeight, _Mapping]] = ..., parent_slot: _Optional[int] = ..., parent_blockhash: _Optional[str] = ..., executed_transaction_count: _Optional[int] = ..., transactions: _Optional[_Iterable[_Union[SubscribeUpdateTransactionInfo, _Mapping]]] = ..., updated_account_count: _Optional[int] = ..., accounts: _Optional[_Iterable[_Union[SubscribeUpdateAccountInfo, _Mapping]]] = ..., entries_count: _Optional[int] = ..., entries: _Optional[_Iterable[_Union[SubscribeUpdateEntry, _Mapping]]] = ...) -> None: ...
class SubscribeUpdateBlockMeta(_message.Message):
__slots__ = ("slot", "blockhash", "rewards", "block_time", "block_height", "parent_slot", "parent_blockhash", "executed_transaction_count", "entries_count")
SLOT_FIELD_NUMBER: _ClassVar[int]
BLOCKHASH_FIELD_NUMBER: _ClassVar[int]
REWARDS_FIELD_NUMBER: _ClassVar[int]
BLOCK_TIME_FIELD_NUMBER: _ClassVar[int]
BLOCK_HEIGHT_FIELD_NUMBER: _ClassVar[int]
PARENT_SLOT_FIELD_NUMBER: _ClassVar[int]
PARENT_BLOCKHASH_FIELD_NUMBER: _ClassVar[int]
EXECUTED_TRANSACTION_COUNT_FIELD_NUMBER: _ClassVar[int]
ENTRIES_COUNT_FIELD_NUMBER: _ClassVar[int]
slot: int
blockhash: str
rewards: _solana_storage_pb2.Rewards
block_time: _solana_storage_pb2.UnixTimestamp
block_height: _solana_storage_pb2.BlockHeight
parent_slot: int
parent_blockhash: str
executed_transaction_count: int
entries_count: int
def __init__(self, slot: _Optional[int] = ..., blockhash: _Optional[str] = ..., rewards: _Optional[_Union[_solana_storage_pb2.Rewards, _Mapping]] = ..., block_time: _Optional[_Union[_solana_storage_pb2.UnixTimestamp, _Mapping]] = ..., block_height: _Optional[_Union[_solana_storage_pb2.BlockHeight, _Mapping]] = ..., parent_slot: _Optional[int] = ..., parent_blockhash: _Optional[str] = ..., executed_transaction_count: _Optional[int] = ..., entries_count: _Optional[int] = ...) -> None: ...
class SubscribeUpdateEntry(_message.Message):
__slots__ = ("slot", "index", "num_hashes", "hash", "executed_transaction_count", "starting_transaction_index")
SLOT_FIELD_NUMBER: _ClassVar[int]
INDEX_FIELD_NUMBER: _ClassVar[int]
NUM_HASHES_FIELD_NUMBER: _ClassVar[int]
HASH_FIELD_NUMBER: _ClassVar[int]
EXECUTED_TRANSACTION_COUNT_FIELD_NUMBER: _ClassVar[int]
STARTING_TRANSACTION_INDEX_FIELD_NUMBER: _ClassVar[int]
slot: int
index: int
num_hashes: int
hash: bytes
executed_transaction_count: int
starting_transaction_index: int
def __init__(self, slot: _Optional[int] = ..., index: _Optional[int] = ..., num_hashes: _Optional[int] = ..., hash: _Optional[bytes] = ..., executed_transaction_count: _Optional[int] = ..., starting_transaction_index: _Optional[int] = ...) -> None: ...
class SubscribeUpdatePing(_message.Message):
__slots__ = ()
def __init__(self) -> None: ...
class SubscribeUpdatePong(_message.Message):
__slots__ = ("id",)
ID_FIELD_NUMBER: _ClassVar[int]
id: int
def __init__(self, id: _Optional[int] = ...) -> None: ...
class PingRequest(_message.Message):
__slots__ = ("count",)
COUNT_FIELD_NUMBER: _ClassVar[int]
count: int
def __init__(self, count: _Optional[int] = ...) -> None: ...
class PongResponse(_message.Message):
__slots__ = ("count",)
COUNT_FIELD_NUMBER: _ClassVar[int]
count: int
def __init__(self, count: _Optional[int] = ...) -> None: ...
class GetLatestBlockhashRequest(_message.Message):
__slots__ = ("commitment",)
COMMITMENT_FIELD_NUMBER: _ClassVar[int]
commitment: CommitmentLevel
def __init__(self, commitment: _Optional[_Union[CommitmentLevel, str]] = ...) -> None: ...
class GetLatestBlockhashResponse(_message.Message):
__slots__ = ("slot", "blockhash", "last_valid_block_height")
SLOT_FIELD_NUMBER: _ClassVar[int]
BLOCKHASH_FIELD_NUMBER: _ClassVar[int]
LAST_VALID_BLOCK_HEIGHT_FIELD_NUMBER: _ClassVar[int]
slot: int
blockhash: str
last_valid_block_height: int
def __init__(self, slot: _Optional[int] = ..., blockhash: _Optional[str] = ..., last_valid_block_height: _Optional[int] = ...) -> None: ...
class GetBlockHeightRequest(_message.Message):
__slots__ = ("commitment",)
COMMITMENT_FIELD_NUMBER: _ClassVar[int]
commitment: CommitmentLevel
def __init__(self, commitment: _Optional[_Union[CommitmentLevel, str]] = ...) -> None: ...
class GetBlockHeightResponse(_message.Message):
__slots__ = ("block_height",)
BLOCK_HEIGHT_FIELD_NUMBER: _ClassVar[int]
block_height: int
def __init__(self, block_height: _Optional[int] = ...) -> None: ...
class GetSlotRequest(_message.Message):
__slots__ = ("commitment",)
COMMITMENT_FIELD_NUMBER: _ClassVar[int]
commitment: CommitmentLevel
def __init__(self, commitment: _Optional[_Union[CommitmentLevel, str]] = ...) -> None: ...
class GetSlotResponse(_message.Message):
__slots__ = ("slot",)
SLOT_FIELD_NUMBER: _ClassVar[int]
slot: int
def __init__(self, slot: _Optional[int] = ...) -> None: ...
class GetVersionRequest(_message.Message):
__slots__ = ()
def __init__(self) -> None: ...
class GetVersionResponse(_message.Message):
__slots__ = ("version",)
VERSION_FIELD_NUMBER: _ClassVar[int]
version: str
def __init__(self, version: _Optional[str] = ...) -> None: ...
class IsBlockhashValidRequest(_message.Message):
__slots__ = ("blockhash", "commitment")
BLOCKHASH_FIELD_NUMBER: _ClassVar[int]
COMMITMENT_FIELD_NUMBER: _ClassVar[int]
blockhash: str
commitment: CommitmentLevel
def __init__(self, blockhash: _Optional[str] = ..., commitment: _Optional[_Union[CommitmentLevel, str]] = ...) -> None: ...
class IsBlockhashValidResponse(_message.Message):
__slots__ = ("slot", "valid")
SLOT_FIELD_NUMBER: _ClassVar[int]
VALID_FIELD_NUMBER: _ClassVar[int]
slot: int
valid: bool
def __init__(self, slot: _Optional[int] = ..., valid: bool = ...) -> None: ...
+355
View File
@@ -0,0 +1,355 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
import geyser.generated.geyser_pb2 as geyser__pb2
GRPC_GENERATED_VERSION = '1.71.0'
GRPC_VERSION = grpc.__version__
_version_not_supported = False
try:
from grpc._utilities import first_version_is_lower
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
except ImportError:
_version_not_supported = True
if _version_not_supported:
raise RuntimeError(
f'The grpc package installed is at version {GRPC_VERSION},'
+ ' but the generated code in geyser_pb2_grpc.py depends on'
+ f' 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}.'
)
class GeyserStub:
"""Missing associated documentation comment in .proto file."""
def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.Subscribe = channel.stream_stream(
'/geyser.Geyser/Subscribe',
request_serializer=geyser__pb2.SubscribeRequest.SerializeToString,
response_deserializer=geyser__pb2.SubscribeUpdate.FromString,
_registered_method=True)
self.Ping = channel.unary_unary(
'/geyser.Geyser/Ping',
request_serializer=geyser__pb2.PingRequest.SerializeToString,
response_deserializer=geyser__pb2.PongResponse.FromString,
_registered_method=True)
self.GetLatestBlockhash = channel.unary_unary(
'/geyser.Geyser/GetLatestBlockhash',
request_serializer=geyser__pb2.GetLatestBlockhashRequest.SerializeToString,
response_deserializer=geyser__pb2.GetLatestBlockhashResponse.FromString,
_registered_method=True)
self.GetBlockHeight = channel.unary_unary(
'/geyser.Geyser/GetBlockHeight',
request_serializer=geyser__pb2.GetBlockHeightRequest.SerializeToString,
response_deserializer=geyser__pb2.GetBlockHeightResponse.FromString,
_registered_method=True)
self.GetSlot = channel.unary_unary(
'/geyser.Geyser/GetSlot',
request_serializer=geyser__pb2.GetSlotRequest.SerializeToString,
response_deserializer=geyser__pb2.GetSlotResponse.FromString,
_registered_method=True)
self.IsBlockhashValid = channel.unary_unary(
'/geyser.Geyser/IsBlockhashValid',
request_serializer=geyser__pb2.IsBlockhashValidRequest.SerializeToString,
response_deserializer=geyser__pb2.IsBlockhashValidResponse.FromString,
_registered_method=True)
self.GetVersion = channel.unary_unary(
'/geyser.Geyser/GetVersion',
request_serializer=geyser__pb2.GetVersionRequest.SerializeToString,
response_deserializer=geyser__pb2.GetVersionResponse.FromString,
_registered_method=True)
class GeyserServicer:
"""Missing associated documentation comment in .proto file."""
def Subscribe(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 Ping(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 GetLatestBlockhash(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 GetBlockHeight(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 GetSlot(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 IsBlockhashValid(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 GetVersion(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 add_GeyserServicer_to_server(servicer, server):
rpc_method_handlers = {
'Subscribe': grpc.stream_stream_rpc_method_handler(
servicer.Subscribe,
request_deserializer=geyser__pb2.SubscribeRequest.FromString,
response_serializer=geyser__pb2.SubscribeUpdate.SerializeToString,
),
'Ping': grpc.unary_unary_rpc_method_handler(
servicer.Ping,
request_deserializer=geyser__pb2.PingRequest.FromString,
response_serializer=geyser__pb2.PongResponse.SerializeToString,
),
'GetLatestBlockhash': grpc.unary_unary_rpc_method_handler(
servicer.GetLatestBlockhash,
request_deserializer=geyser__pb2.GetLatestBlockhashRequest.FromString,
response_serializer=geyser__pb2.GetLatestBlockhashResponse.SerializeToString,
),
'GetBlockHeight': grpc.unary_unary_rpc_method_handler(
servicer.GetBlockHeight,
request_deserializer=geyser__pb2.GetBlockHeightRequest.FromString,
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(
'geyser.Geyser', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))
server.add_registered_method_handlers('geyser.Geyser', rpc_method_handlers)
# This class is part of an EXPERIMENTAL API.
class Geyser:
"""Missing associated documentation comment in .proto file."""
@staticmethod
def Subscribe(request_iterator,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.stream_stream(
request_iterator,
target,
'/geyser.Geyser/Subscribe',
geyser__pb2.SubscribeRequest.SerializeToString,
geyser__pb2.SubscribeUpdate.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.PongResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def GetLatestBlockhash(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/GetLatestBlockhash',
geyser__pb2.GetLatestBlockhashRequest.SerializeToString,
geyser__pb2.GetLatestBlockhashResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def GetBlockHeight(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/GetBlockHeight',
geyser__pb2.GetBlockHeightRequest.SerializeToString,
geyser__pb2.GetBlockHeightResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def GetSlot(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/GetSlot',
geyser__pb2.GetSlotRequest.SerializeToString,
geyser__pb2.GetSlotResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def IsBlockhashValid(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/IsBlockhashValid',
geyser__pb2.IsBlockhashValidRequest.SerializeToString,
geyser__pb2.IsBlockhashValidResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
@staticmethod
def GetVersion(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/GetVersion',
geyser__pb2.GetVersionRequest.SerializeToString,
geyser__pb2.GetVersionResponse.FromString,
options,
channel_credentials,
insecure,
call_credentials,
compression,
wait_for_ready,
timeout,
metadata,
_registered_method=True)
File diff suppressed because one or more lines are too long
+238
View File
@@ -0,0 +1,238 @@
from google.protobuf.internal import containers as _containers
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
DESCRIPTOR: _descriptor.FileDescriptor
class RewardType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = ()
Unspecified: _ClassVar[RewardType]
Fee: _ClassVar[RewardType]
Rent: _ClassVar[RewardType]
Staking: _ClassVar[RewardType]
Voting: _ClassVar[RewardType]
Unspecified: RewardType
Fee: RewardType
Rent: RewardType
Staking: RewardType
Voting: RewardType
class ConfirmedBlock(_message.Message):
__slots__ = ("previous_blockhash", "blockhash", "parent_slot", "transactions", "rewards", "block_time", "block_height", "num_partitions")
PREVIOUS_BLOCKHASH_FIELD_NUMBER: _ClassVar[int]
BLOCKHASH_FIELD_NUMBER: _ClassVar[int]
PARENT_SLOT_FIELD_NUMBER: _ClassVar[int]
TRANSACTIONS_FIELD_NUMBER: _ClassVar[int]
REWARDS_FIELD_NUMBER: _ClassVar[int]
BLOCK_TIME_FIELD_NUMBER: _ClassVar[int]
BLOCK_HEIGHT_FIELD_NUMBER: _ClassVar[int]
NUM_PARTITIONS_FIELD_NUMBER: _ClassVar[int]
previous_blockhash: str
blockhash: str
parent_slot: int
transactions: _containers.RepeatedCompositeFieldContainer[ConfirmedTransaction]
rewards: _containers.RepeatedCompositeFieldContainer[Reward]
block_time: UnixTimestamp
block_height: BlockHeight
num_partitions: NumPartitions
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: ...
class ConfirmedTransaction(_message.Message):
__slots__ = ("transaction", "meta")
TRANSACTION_FIELD_NUMBER: _ClassVar[int]
META_FIELD_NUMBER: _ClassVar[int]
transaction: Transaction
meta: TransactionStatusMeta
def __init__(self, transaction: _Optional[_Union[Transaction, _Mapping]] = ..., meta: _Optional[_Union[TransactionStatusMeta, _Mapping]] = ...) -> None: ...
class Transaction(_message.Message):
__slots__ = ("signatures", "message")
SIGNATURES_FIELD_NUMBER: _ClassVar[int]
MESSAGE_FIELD_NUMBER: _ClassVar[int]
signatures: _containers.RepeatedScalarFieldContainer[bytes]
message: Message
def __init__(self, signatures: _Optional[_Iterable[bytes]] = ..., message: _Optional[_Union[Message, _Mapping]] = ...) -> None: ...
class Message(_message.Message):
__slots__ = ("header", "account_keys", "recent_blockhash", "instructions", "versioned", "address_table_lookups")
HEADER_FIELD_NUMBER: _ClassVar[int]
ACCOUNT_KEYS_FIELD_NUMBER: _ClassVar[int]
RECENT_BLOCKHASH_FIELD_NUMBER: _ClassVar[int]
INSTRUCTIONS_FIELD_NUMBER: _ClassVar[int]
VERSIONED_FIELD_NUMBER: _ClassVar[int]
ADDRESS_TABLE_LOOKUPS_FIELD_NUMBER: _ClassVar[int]
header: MessageHeader
account_keys: _containers.RepeatedScalarFieldContainer[bytes]
recent_blockhash: bytes
instructions: _containers.RepeatedCompositeFieldContainer[CompiledInstruction]
versioned: bool
address_table_lookups: _containers.RepeatedCompositeFieldContainer[MessageAddressTableLookup]
def __init__(self, header: _Optional[_Union[MessageHeader, _Mapping]] = ..., account_keys: _Optional[_Iterable[bytes]] = ..., recent_blockhash: _Optional[bytes] = ..., instructions: _Optional[_Iterable[_Union[CompiledInstruction, _Mapping]]] = ..., versioned: bool = ..., address_table_lookups: _Optional[_Iterable[_Union[MessageAddressTableLookup, _Mapping]]] = ...) -> None: ...
class MessageHeader(_message.Message):
__slots__ = ("num_required_signatures", "num_readonly_signed_accounts", "num_readonly_unsigned_accounts")
NUM_REQUIRED_SIGNATURES_FIELD_NUMBER: _ClassVar[int]
NUM_READONLY_SIGNED_ACCOUNTS_FIELD_NUMBER: _ClassVar[int]
NUM_READONLY_UNSIGNED_ACCOUNTS_FIELD_NUMBER: _ClassVar[int]
num_required_signatures: int
num_readonly_signed_accounts: int
num_readonly_unsigned_accounts: int
def __init__(self, num_required_signatures: _Optional[int] = ..., num_readonly_signed_accounts: _Optional[int] = ..., num_readonly_unsigned_accounts: _Optional[int] = ...) -> None: ...
class MessageAddressTableLookup(_message.Message):
__slots__ = ("account_key", "writable_indexes", "readonly_indexes")
ACCOUNT_KEY_FIELD_NUMBER: _ClassVar[int]
WRITABLE_INDEXES_FIELD_NUMBER: _ClassVar[int]
READONLY_INDEXES_FIELD_NUMBER: _ClassVar[int]
account_key: bytes
writable_indexes: bytes
readonly_indexes: bytes
def __init__(self, account_key: _Optional[bytes] = ..., writable_indexes: _Optional[bytes] = ..., readonly_indexes: _Optional[bytes] = ...) -> None: ...
class TransactionStatusMeta(_message.Message):
__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")
ERR_FIELD_NUMBER: _ClassVar[int]
FEE_FIELD_NUMBER: _ClassVar[int]
PRE_BALANCES_FIELD_NUMBER: _ClassVar[int]
POST_BALANCES_FIELD_NUMBER: _ClassVar[int]
INNER_INSTRUCTIONS_FIELD_NUMBER: _ClassVar[int]
INNER_INSTRUCTIONS_NONE_FIELD_NUMBER: _ClassVar[int]
LOG_MESSAGES_FIELD_NUMBER: _ClassVar[int]
LOG_MESSAGES_NONE_FIELD_NUMBER: _ClassVar[int]
PRE_TOKEN_BALANCES_FIELD_NUMBER: _ClassVar[int]
POST_TOKEN_BALANCES_FIELD_NUMBER: _ClassVar[int]
REWARDS_FIELD_NUMBER: _ClassVar[int]
LOADED_WRITABLE_ADDRESSES_FIELD_NUMBER: _ClassVar[int]
LOADED_READONLY_ADDRESSES_FIELD_NUMBER: _ClassVar[int]
RETURN_DATA_FIELD_NUMBER: _ClassVar[int]
RETURN_DATA_NONE_FIELD_NUMBER: _ClassVar[int]
COMPUTE_UNITS_CONSUMED_FIELD_NUMBER: _ClassVar[int]
err: TransactionError
fee: int
pre_balances: _containers.RepeatedScalarFieldContainer[int]
post_balances: _containers.RepeatedScalarFieldContainer[int]
inner_instructions: _containers.RepeatedCompositeFieldContainer[InnerInstructions]
inner_instructions_none: bool
log_messages: _containers.RepeatedScalarFieldContainer[str]
log_messages_none: bool
pre_token_balances: _containers.RepeatedCompositeFieldContainer[TokenBalance]
post_token_balances: _containers.RepeatedCompositeFieldContainer[TokenBalance]
rewards: _containers.RepeatedCompositeFieldContainer[Reward]
loaded_writable_addresses: _containers.RepeatedScalarFieldContainer[bytes]
loaded_readonly_addresses: _containers.RepeatedScalarFieldContainer[bytes]
return_data: ReturnData
return_data_none: bool
compute_units_consumed: int
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: 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):
__slots__ = ("err",)
ERR_FIELD_NUMBER: _ClassVar[int]
err: bytes
def __init__(self, err: _Optional[bytes] = ...) -> None: ...
class InnerInstructions(_message.Message):
__slots__ = ("index", "instructions")
INDEX_FIELD_NUMBER: _ClassVar[int]
INSTRUCTIONS_FIELD_NUMBER: _ClassVar[int]
index: int
instructions: _containers.RepeatedCompositeFieldContainer[InnerInstruction]
def __init__(self, index: _Optional[int] = ..., instructions: _Optional[_Iterable[_Union[InnerInstruction, _Mapping]]] = ...) -> None: ...
class InnerInstruction(_message.Message):
__slots__ = ("program_id_index", "accounts", "data", "stack_height")
PROGRAM_ID_INDEX_FIELD_NUMBER: _ClassVar[int]
ACCOUNTS_FIELD_NUMBER: _ClassVar[int]
DATA_FIELD_NUMBER: _ClassVar[int]
STACK_HEIGHT_FIELD_NUMBER: _ClassVar[int]
program_id_index: int
accounts: bytes
data: bytes
stack_height: int
def __init__(self, program_id_index: _Optional[int] = ..., accounts: _Optional[bytes] = ..., data: _Optional[bytes] = ..., stack_height: _Optional[int] = ...) -> None: ...
class CompiledInstruction(_message.Message):
__slots__ = ("program_id_index", "accounts", "data")
PROGRAM_ID_INDEX_FIELD_NUMBER: _ClassVar[int]
ACCOUNTS_FIELD_NUMBER: _ClassVar[int]
DATA_FIELD_NUMBER: _ClassVar[int]
program_id_index: int
accounts: bytes
data: bytes
def __init__(self, program_id_index: _Optional[int] = ..., accounts: _Optional[bytes] = ..., data: _Optional[bytes] = ...) -> None: ...
class TokenBalance(_message.Message):
__slots__ = ("account_index", "mint", "ui_token_amount", "owner", "program_id")
ACCOUNT_INDEX_FIELD_NUMBER: _ClassVar[int]
MINT_FIELD_NUMBER: _ClassVar[int]
UI_TOKEN_AMOUNT_FIELD_NUMBER: _ClassVar[int]
OWNER_FIELD_NUMBER: _ClassVar[int]
PROGRAM_ID_FIELD_NUMBER: _ClassVar[int]
account_index: int
mint: str
ui_token_amount: UiTokenAmount
owner: str
program_id: str
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: ...
class UiTokenAmount(_message.Message):
__slots__ = ("ui_amount", "decimals", "amount", "ui_amount_string")
UI_AMOUNT_FIELD_NUMBER: _ClassVar[int]
DECIMALS_FIELD_NUMBER: _ClassVar[int]
AMOUNT_FIELD_NUMBER: _ClassVar[int]
UI_AMOUNT_STRING_FIELD_NUMBER: _ClassVar[int]
ui_amount: float
decimals: int
amount: str
ui_amount_string: str
def __init__(self, ui_amount: _Optional[float] = ..., decimals: _Optional[int] = ..., amount: _Optional[str] = ..., ui_amount_string: _Optional[str] = ...) -> None: ...
class ReturnData(_message.Message):
__slots__ = ("program_id", "data")
PROGRAM_ID_FIELD_NUMBER: _ClassVar[int]
DATA_FIELD_NUMBER: _ClassVar[int]
program_id: bytes
data: bytes
def __init__(self, program_id: _Optional[bytes] = ..., data: _Optional[bytes] = ...) -> None: ...
class Reward(_message.Message):
__slots__ = ("pubkey", "lamports", "post_balance", "reward_type", "commission")
PUBKEY_FIELD_NUMBER: _ClassVar[int]
LAMPORTS_FIELD_NUMBER: _ClassVar[int]
POST_BALANCE_FIELD_NUMBER: _ClassVar[int]
REWARD_TYPE_FIELD_NUMBER: _ClassVar[int]
COMMISSION_FIELD_NUMBER: _ClassVar[int]
pubkey: str
lamports: int
post_balance: int
reward_type: RewardType
commission: str
def __init__(self, pubkey: _Optional[str] = ..., lamports: _Optional[int] = ..., post_balance: _Optional[int] = ..., reward_type: _Optional[_Union[RewardType, str]] = ..., commission: _Optional[str] = ...) -> None: ...
class Rewards(_message.Message):
__slots__ = ("rewards", "num_partitions")
REWARDS_FIELD_NUMBER: _ClassVar[int]
NUM_PARTITIONS_FIELD_NUMBER: _ClassVar[int]
rewards: _containers.RepeatedCompositeFieldContainer[Reward]
num_partitions: NumPartitions
def __init__(self, rewards: _Optional[_Iterable[_Union[Reward, _Mapping]]] = ..., num_partitions: _Optional[_Union[NumPartitions, _Mapping]] = ...) -> None: ...
class UnixTimestamp(_message.Message):
__slots__ = ("timestamp",)
TIMESTAMP_FIELD_NUMBER: _ClassVar[int]
timestamp: int
def __init__(self, timestamp: _Optional[int] = ...) -> None: ...
class BlockHeight(_message.Message):
__slots__ = ("block_height",)
BLOCK_HEIGHT_FIELD_NUMBER: _ClassVar[int]
block_height: int
def __init__(self, block_height: _Optional[int] = ...) -> None: ...
class NumPartitions(_message.Message):
__slots__ = ("num_partitions",)
NUM_PARTITIONS_FIELD_NUMBER: _ClassVar[int]
num_partitions: int
def __init__(self, num_partitions: _Optional[int] = ...) -> None: ...
@@ -0,0 +1,24 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
import warnings
GRPC_GENERATED_VERSION = '1.71.0'
GRPC_VERSION = grpc.__version__
_version_not_supported = False
try:
from grpc._utilities import first_version_is_lower
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
except ImportError:
_version_not_supported = True
if _version_not_supported:
raise RuntimeError(
f'The grpc package installed is at version {GRPC_VERSION},'
+ f' but the generated code in solana_storage_pb2_grpc.py depends on'
+ f' 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}.'
)
+262
View File
@@ -0,0 +1,262 @@
syntax = "proto3";
import public "solana-storage.proto";
option go_package = "github.com/rpcpool/yellowstone-grpc/examples/golang/proto";
package geyser;
service Geyser {
rpc Subscribe(stream SubscribeRequest) returns (stream SubscribeUpdate) {}
rpc Ping(PingRequest) returns (PongResponse) {}
rpc GetLatestBlockhash(GetLatestBlockhashRequest) returns (GetLatestBlockhashResponse) {}
rpc GetBlockHeight(GetBlockHeightRequest) returns (GetBlockHeightResponse) {}
rpc GetSlot(GetSlotRequest) returns (GetSlotResponse) {}
rpc IsBlockhashValid(IsBlockhashValidRequest) returns (IsBlockhashValidResponse) {}
rpc GetVersion(GetVersionRequest) returns (GetVersionResponse) {}
}
enum CommitmentLevel {
PROCESSED = 0;
CONFIRMED = 1;
FINALIZED = 2;
FIRST_SHRED_RECEIVED = 3;
COMPLETED = 4;
CREATED_BANK = 5;
DEAD = 6;
}
message SubscribeRequest {
map<string, SubscribeRequestFilterAccounts> accounts = 1;
map<string, SubscribeRequestFilterSlots> slots = 2;
map<string, SubscribeRequestFilterTransactions> transactions = 3;
map<string, SubscribeRequestFilterTransactions> transactions_status = 10;
map<string, SubscribeRequestFilterBlocks> blocks = 4;
map<string, SubscribeRequestFilterBlocksMeta> blocks_meta = 5;
map<string, SubscribeRequestFilterEntry> entry = 8;
optional CommitmentLevel commitment = 6;
repeated SubscribeRequestAccountsDataSlice accounts_data_slice = 7;
optional SubscribeRequestPing ping = 9;
}
message SubscribeRequestFilterAccounts {
repeated string account = 2;
repeated string owner = 3;
repeated SubscribeRequestFilterAccountsFilter filters = 4;
optional bool nonempty_txn_signature = 5;
}
message SubscribeRequestFilterAccountsFilter {
oneof filter {
SubscribeRequestFilterAccountsFilterMemcmp memcmp = 1;
uint64 datasize = 2;
bool token_account_state = 3;
SubscribeRequestFilterAccountsFilterLamports lamports = 4;
}
}
message SubscribeRequestFilterAccountsFilterMemcmp {
uint64 offset = 1;
oneof data {
bytes bytes = 2;
string base58 = 3;
string base64 = 4;
}
}
message SubscribeRequestFilterAccountsFilterLamports {
oneof cmp {
uint64 eq = 1;
uint64 ne = 2;
uint64 lt = 3;
uint64 gt = 4;
}
}
message SubscribeRequestFilterSlots {
optional bool filter_by_commitment = 1;
}
message SubscribeRequestFilterTransactions {
optional bool vote = 1;
optional bool failed = 2;
optional string signature = 5;
repeated string account_include = 3;
repeated string account_exclude = 4;
repeated string account_required = 6;
}
message SubscribeRequestFilterBlocks {
repeated string account_include = 1;
optional bool include_transactions = 2;
optional bool include_accounts = 3;
optional bool include_entries = 4;
}
message SubscribeRequestFilterBlocksMeta {}
message SubscribeRequestFilterEntry {}
message SubscribeRequestAccountsDataSlice {
uint64 offset = 1;
uint64 length = 2;
}
message SubscribeRequestPing {
int32 id = 1;
}
message SubscribeUpdate {
repeated string filters = 1;
oneof update_oneof {
SubscribeUpdateAccount account = 2;
SubscribeUpdateSlot slot = 3;
SubscribeUpdateTransaction transaction = 4;
SubscribeUpdateTransactionStatus transaction_status = 10;
SubscribeUpdateBlock block = 5;
SubscribeUpdatePing ping = 6;
SubscribeUpdatePong pong = 9;
SubscribeUpdateBlockMeta block_meta = 7;
SubscribeUpdateEntry entry = 8;
}
}
message SubscribeUpdateAccount {
SubscribeUpdateAccountInfo account = 1;
uint64 slot = 2;
bool is_startup = 3;
}
message SubscribeUpdateAccountInfo {
bytes pubkey = 1;
uint64 lamports = 2;
bytes owner = 3;
bool executable = 4;
uint64 rent_epoch = 5;
bytes data = 6;
uint64 write_version = 7;
optional bytes txn_signature = 8;
}
message SubscribeUpdateSlot {
uint64 slot = 1;
optional uint64 parent = 2;
CommitmentLevel status = 3;
optional string dead_error = 4;
}
message SubscribeUpdateTransaction {
SubscribeUpdateTransactionInfo transaction = 1;
uint64 slot = 2;
}
message SubscribeUpdateTransactionInfo {
bytes signature = 1;
bool is_vote = 2;
solana.storage.ConfirmedBlock.Transaction transaction = 3;
solana.storage.ConfirmedBlock.TransactionStatusMeta meta = 4;
uint64 index = 5;
}
message SubscribeUpdateTransactionStatus {
uint64 slot = 1;
bytes signature = 2;
bool is_vote = 3;
uint64 index = 4;
solana.storage.ConfirmedBlock.TransactionError err = 5;
}
message SubscribeUpdateBlock {
uint64 slot = 1;
string blockhash = 2;
solana.storage.ConfirmedBlock.Rewards rewards = 3;
solana.storage.ConfirmedBlock.UnixTimestamp block_time = 4;
solana.storage.ConfirmedBlock.BlockHeight block_height = 5;
uint64 parent_slot = 7;
string parent_blockhash = 8;
uint64 executed_transaction_count = 9;
repeated SubscribeUpdateTransactionInfo transactions = 6;
uint64 updated_account_count = 10;
repeated SubscribeUpdateAccountInfo accounts = 11;
uint64 entries_count = 12;
repeated SubscribeUpdateEntry entries = 13;
}
message SubscribeUpdateBlockMeta {
uint64 slot = 1;
string blockhash = 2;
solana.storage.ConfirmedBlock.Rewards rewards = 3;
solana.storage.ConfirmedBlock.UnixTimestamp block_time = 4;
solana.storage.ConfirmedBlock.BlockHeight block_height = 5;
uint64 parent_slot = 6;
string parent_blockhash = 7;
uint64 executed_transaction_count = 8;
uint64 entries_count = 9;
}
message SubscribeUpdateEntry {
uint64 slot = 1;
uint64 index = 2;
uint64 num_hashes = 3;
bytes hash = 4;
uint64 executed_transaction_count = 5;
uint64 starting_transaction_index = 6; // added in v1.18, for solana 1.17 value is always 0
}
message SubscribeUpdatePing {}
message SubscribeUpdatePong {
int32 id = 1;
}
// non-streaming methods
message PingRequest {
int32 count = 1;
}
message PongResponse {
int32 count = 1;
}
message GetLatestBlockhashRequest {
optional CommitmentLevel commitment = 1;
}
message GetLatestBlockhashResponse {
uint64 slot = 1;
string blockhash = 2;
uint64 last_valid_block_height = 3;
}
message GetBlockHeightRequest {
optional CommitmentLevel commitment = 1;
}
message GetBlockHeightResponse {
uint64 block_height = 1;
}
message GetSlotRequest {
optional CommitmentLevel commitment = 1;
}
message GetSlotResponse {
uint64 slot = 1;
}
message GetVersionRequest {}
message GetVersionResponse {
string version = 1;
}
message IsBlockhashValidRequest {
string blockhash = 1;
optional CommitmentLevel commitment = 2;
}
message IsBlockhashValidResponse {
uint64 slot = 1;
bool valid = 2;
}
+149
View File
@@ -0,0 +1,149 @@
syntax = "proto3";
package solana.storage.ConfirmedBlock;
option go_package = "github.com/rpcpool/yellowstone-grpc/examples/golang/proto";
message ConfirmedBlock {
string previous_blockhash = 1;
string blockhash = 2;
uint64 parent_slot = 3;
repeated ConfirmedTransaction transactions = 4;
repeated Reward rewards = 5;
UnixTimestamp block_time = 6;
BlockHeight block_height = 7;
NumPartitions num_partitions = 8;
}
message ConfirmedTransaction {
Transaction transaction = 1;
TransactionStatusMeta meta = 2;
}
message Transaction {
repeated bytes signatures = 1;
Message message = 2;
}
message Message {
MessageHeader header = 1;
repeated bytes account_keys = 2;
bytes recent_blockhash = 3;
repeated CompiledInstruction instructions = 4;
bool versioned = 5;
repeated MessageAddressTableLookup address_table_lookups = 6;
}
message MessageHeader {
uint32 num_required_signatures = 1;
uint32 num_readonly_signed_accounts = 2;
uint32 num_readonly_unsigned_accounts = 3;
}
message MessageAddressTableLookup {
bytes account_key = 1;
bytes writable_indexes = 2;
bytes readonly_indexes = 3;
}
message TransactionStatusMeta {
TransactionError err = 1;
uint64 fee = 2;
repeated uint64 pre_balances = 3;
repeated uint64 post_balances = 4;
repeated InnerInstructions inner_instructions = 5;
bool inner_instructions_none = 10;
repeated string log_messages = 6;
bool log_messages_none = 11;
repeated TokenBalance pre_token_balances = 7;
repeated TokenBalance post_token_balances = 8;
repeated Reward rewards = 9;
repeated bytes loaded_writable_addresses = 12;
repeated bytes loaded_readonly_addresses = 13;
ReturnData return_data = 14;
bool return_data_none = 15;
// Sum of compute units consumed by all instructions.
// Available since Solana v1.10.35 / v1.11.6.
// Set to `None` for txs executed on earlier versions.
optional uint64 compute_units_consumed = 16;
}
message TransactionError {
bytes err = 1;
}
message InnerInstructions {
uint32 index = 1;
repeated InnerInstruction instructions = 2;
}
message InnerInstruction {
uint32 program_id_index = 1;
bytes accounts = 2;
bytes data = 3;
// Invocation stack height of an inner instruction.
// Available since Solana v1.14.6
// Set to `None` for txs executed on earlier versions.
optional uint32 stack_height = 4;
}
message CompiledInstruction {
uint32 program_id_index = 1;
bytes accounts = 2;
bytes data = 3;
}
message TokenBalance {
uint32 account_index = 1;
string mint = 2;
UiTokenAmount ui_token_amount = 3;
string owner = 4;
string program_id = 5;
}
message UiTokenAmount {
double ui_amount = 1;
uint32 decimals = 2;
string amount = 3;
string ui_amount_string = 4;
}
message ReturnData {
bytes program_id = 1;
bytes data = 2;
}
enum RewardType {
Unspecified = 0;
Fee = 1;
Rent = 2;
Staking = 3;
Voting = 4;
}
message Reward {
string pubkey = 1;
int64 lamports = 2;
uint64 post_balance = 3;
RewardType reward_type = 4;
string commission = 5;
}
message Rewards {
repeated Reward rewards = 1;
NumPartitions num_partitions = 2;
}
message UnixTimestamp {
int64 timestamp = 1;
}
message BlockHeight {
uint64 block_height = 1;
}
message NumPartitions {
uint64 num_partitions = 1;
}
View File
+29
View File
@@ -0,0 +1,29 @@
"""
Base class for WebSocket token listeners.
"""
from abc import ABC, abstractmethod
from collections.abc import Awaitable, Callable
from trading.base import TokenInfo
class BaseTokenListener(ABC):
"""Base abstract class for token listeners."""
@abstractmethod
async def listen_for_tokens(
self,
token_callback: Callable[[TokenInfo], Awaitable[None]],
match_string: str | None = None,
creator_address: str | None = None,
) -> None:
"""
Listen for new token creations.
Args:
token_callback: Callback function for new tokens
match_string: Optional string to match in token name/symbol
creator_address: Optional creator address to filter by
"""
pass
+168
View File
@@ -0,0 +1,168 @@
"""
Event processing for pump.fun tokens.
"""
import base64
import json
import struct
from typing import Any
from solders.pubkey import Pubkey
from solders.transaction import VersionedTransaction
from trading.base import TokenInfo
from utils.logger import get_logger
logger = get_logger(__name__)
class PumpEventProcessor:
"""Processes events from pump.fun program."""
# Discriminator for create instruction
CREATE_DISCRIMINATOR = 8576854823835016728
def __init__(self, pump_program: Pubkey):
"""Initialize event processor.
Args:
pump_program: Pump.fun program address
"""
self.pump_program = pump_program
self._idl = self._load_idl()
def _load_idl(self) -> dict[str, Any]:
"""Load IDL from file.
Returns:
IDL as dictionary
"""
try:
with open("idl/pump_fun_idl.json") as f:
return json.load(f)
except Exception as e:
logger.error(f"Failed to load IDL: {str(e)}")
# Create a minimal IDL with just what we need
return {
"instructions": [
{
"name": "create",
"args": [
{"name": "name", "type": "string"},
{"name": "symbol", "type": "string"},
{"name": "uri", "type": "string"},
],
}
]
}
def process_transaction(self, tx_data: str) -> TokenInfo | None:
"""Process a transaction and extract token info.
Args:
tx_data: Base64 encoded transaction data
Returns:
TokenInfo if a token creation is found, None otherwise
"""
try:
tx_data_decoded = base64.b64decode(tx_data)
transaction = VersionedTransaction.from_bytes(tx_data_decoded)
for ix in transaction.message.instructions:
# Check if instruction is from pump.fun program
program_id_index = ix.program_id_index
if program_id_index >= len(transaction.message.account_keys):
continue
program_id = transaction.message.account_keys[program_id_index]
if str(program_id) != str(self.pump_program):
continue
ix_data = bytes(ix.data)
# Check if it's a create instruction
if len(ix_data) < 8:
continue
discriminator = struct.unpack("<Q", ix_data[:8])[0]
if discriminator != self.CREATE_DISCRIMINATOR:
continue
# Found a create instruction, decode it
create_ix = next(
(
instr
for instr in self._idl["instructions"]
if instr["name"] == "create"
),
None,
)
if not create_ix:
continue
# Get account keys for this instruction
account_keys = [
transaction.message.account_keys[index] for index in ix.accounts
]
# Decode instruction arguments
decoded_args = self._decode_create_instruction(
ix_data, create_ix, account_keys
)
return TokenInfo(
name=decoded_args["name"],
symbol=decoded_args["symbol"],
uri=decoded_args["uri"],
mint=Pubkey.from_string(decoded_args["mint"]),
bonding_curve=Pubkey.from_string(decoded_args["bondingCurve"]),
associated_bonding_curve=Pubkey.from_string(
decoded_args["associatedBondingCurve"]
),
user=Pubkey.from_string(decoded_args["user"]),
)
except Exception as e:
logger.error(f"Error processing transaction: {str(e)}")
return None
def _decode_create_instruction(
self, ix_data: bytes, ix_def: dict[str, Any], accounts: list[Pubkey]
) -> dict[str, Any]:
"""Decode create instruction data.
Args:
ix_data: Instruction data bytes
ix_def: Instruction definition from IDL
accounts: List of account pubkeys
Returns:
Decoded instruction arguments
"""
args = {}
offset = 8 # Skip 8-byte discriminator
for arg in ix_def["args"]:
if arg["type"] == "string":
length = struct.unpack_from("<I", ix_data, offset)[0]
offset += 4
value = ix_data[offset : offset + length].decode("utf-8")
offset += length
elif arg["type"] == "publicKey":
value = base64.b64encode(ix_data[offset : offset + 32]).decode("utf-8")
offset += 32
else:
logger.warning(f"Unsupported type: {arg['type']}")
value = None
args[arg["name"]] = value
args["mint"] = str(accounts[0])
args["bondingCurve"] = str(accounts[2])
args["associatedBondingCurve"] = str(accounts[3])
args["user"] = str(accounts[7])
return args
+187
View File
@@ -0,0 +1,187 @@
"""
WebSocket monitoring for pump.fun tokens.
"""
import asyncio
import json
from collections.abc import Awaitable, Callable
import websockets
from solders.pubkey import Pubkey
from monitoring.base_listener import BaseTokenListener
from monitoring.block_event_processor import PumpEventProcessor
from trading.base import TokenInfo
from utils.logger import get_logger
logger = get_logger(__name__)
class BlockListener(BaseTokenListener):
"""WebSocket listener for pump.fun token creation events using blockSubscribe."""
def __init__(self, wss_endpoint: str, pump_program: Pubkey):
"""Initialize token listener.
Args:
wss_endpoint: WebSocket endpoint URL
pump_program: Pump.fun program address
"""
self.wss_endpoint = wss_endpoint
self.pump_program = pump_program
self.event_processor = PumpEventProcessor(pump_program)
self.ping_interval = 20 # seconds
async def listen_for_tokens(
self,
token_callback: Callable[[TokenInfo], Awaitable[None]],
match_string: str | None = None,
creator_address: str | None = None,
) -> None:
"""Listen for new token creations.
Args:
token_callback: Callback function for new tokens
match_string: Optional string to match in token name/symbol
creator_address: Optional creator address to filter by
"""
while True:
try:
async with websockets.connect(self.wss_endpoint) as websocket:
await self._subscribe_to_program(websocket)
ping_task = asyncio.create_task(self._ping_loop(websocket))
try:
while True:
token_info = await self._wait_for_token_creation(websocket)
if not token_info:
continue
logger.info(
f"New token detected: {token_info.name} ({token_info.symbol})"
)
if match_string and not (
match_string.lower() in token_info.name.lower()
or match_string.lower() in token_info.symbol.lower()
):
logger.info(
f"Token does not match filter '{match_string}'. Skipping..."
)
continue
if (
creator_address
and str(token_info.user) != creator_address
):
logger.info(
f"Token not created by {creator_address}. Skipping..."
)
continue
await token_callback(token_info)
except websockets.exceptions.ConnectionClosed:
logger.warning("WebSocket connection closed. Reconnecting...")
ping_task.cancel()
except Exception as e:
logger.error(f"WebSocket connection error: {e!s}")
logger.info("Reconnecting in 5 seconds...")
await asyncio.sleep(5)
async def _subscribe_to_program(self, websocket) -> None:
"""Subscribe to blocks mentioning the pump.fun program.
Args:
websocket: Active WebSocket connection
"""
subscription_message = json.dumps(
{
"jsonrpc": "2.0",
"id": 1,
"method": "blockSubscribe",
"params": [
{"mentionsAccountOrProgram": str(self.pump_program)},
{
"commitment": "confirmed",
"encoding": "base64", # base64 is faster than other encoding options
"showRewards": False,
"transactionDetails": "full",
"maxSupportedTransactionVersion": 0,
},
],
}
)
await websocket.send(subscription_message)
logger.info(f"Subscribed to blocks mentioning program: {self.pump_program}")
async def _ping_loop(self, websocket) -> None:
"""Keep connection alive with pings.
Args:
websocket: Active WebSocket connection
"""
try:
while True:
await asyncio.sleep(self.ping_interval)
try:
pong_waiter = await websocket.ping()
await asyncio.wait_for(pong_waiter, timeout=10)
except TimeoutError:
logger.warning("Ping timeout - server not responding")
# Force reconnection
await websocket.close()
return
except asyncio.CancelledError:
pass
except Exception as e:
logger.error(f"Ping error: {e!s}")
async def _wait_for_token_creation(self, websocket) -> TokenInfo | None:
"""Wait for token creation event.
Args:
websocket: Active WebSocket connection
Returns:
TokenInfo if a token creation is found, None otherwise
"""
try:
response = await asyncio.wait_for(websocket.recv(), timeout=30)
data = json.loads(response)
if "method" not in data or data["method"] != "blockNotification":
return None
if "params" not in data or "result" not in data["params"]:
return None
block_data = data["params"]["result"]
if "value" not in block_data or "block" not in block_data["value"]:
return None
block = block_data["value"]["block"]
if "transactions" not in block:
return None
for tx in block["transactions"]:
if not isinstance(tx, dict) or "transaction" not in tx:
continue
token_info = self.event_processor.process_transaction(
tx["transaction"][0]
)
if token_info:
return token_info
except TimeoutError:
logger.debug("No data received for 30 seconds")
except websockets.exceptions.ConnectionClosed:
logger.warning("WebSocket connection closed")
raise
except Exception as e:
logger.error(f"Error processing WebSocket message: {e!s}")
return None
+92
View File
@@ -0,0 +1,92 @@
"""
Event processing for pump.fun tokens using Geyser data.
"""
import struct
from typing import Final
from solders.pubkey import Pubkey
from trading.base import TokenInfo
from utils.logger import get_logger
logger = get_logger(__name__)
class GeyserEventProcessor:
"""Processes token creation events from Geyser stream."""
CREATE_DISCRIMINATOR: Final[bytes] = struct.pack("<Q", 8576854823835016728)
def __init__(self, pump_program: Pubkey):
"""Initialize event processor.
Args:
pump_program: Pump.fun program address
"""
self.pump_program = pump_program
def process_transaction_data(self, instruction_data: bytes, accounts: list, keys: list) -> TokenInfo | None:
"""Process transaction data and extract token creation info.
Args:
instruction_data: Raw instruction data
accounts: List of account indices
keys: List of account public keys
Returns:
TokenInfo if token creation found, None otherwise
"""
if not instruction_data.startswith(self.CREATE_DISCRIMINATOR):
return None
try:
# Skip past the 8-byte discriminator
offset = 8
# Helper to read strings (prefixed with length)
def read_string():
nonlocal offset
# Get string length (4-byte uint)
length = struct.unpack_from("<I", instruction_data, offset)[0]
offset += 4
# Extract and decode the string
value = instruction_data[offset:offset + length].decode("utf-8")
offset += length
return value
# Helper to get account key
def get_account_key(index):
if index >= len(accounts):
return None
account_index = accounts[index]
if account_index >= len(keys):
return None
return Pubkey.from_bytes(keys[account_index])
name = read_string()
symbol = read_string()
uri = read_string()
mint = get_account_key(0)
bonding_curve = get_account_key(2)
associated_bonding_curve = get_account_key(3)
user = get_account_key(7)
if not all([mint, bonding_curve, associated_bonding_curve, user]):
logger.warning("Missing required account keys in token creation")
return None
return TokenInfo(
name=name,
symbol=symbol,
uri=uri,
mint=mint,
bonding_curve=bonding_curve,
associated_bonding_curve=associated_bonding_curve,
user=user,
)
except Exception as e:
logger.error(f"Failed to process transaction data: {e}")
return None
+159
View File
@@ -0,0 +1,159 @@
"""
Geyser monitoring for pump.fun tokens.
"""
import asyncio
from collections.abc import Awaitable, Callable
import grpc
from solders.pubkey import Pubkey
from geyser.generated import geyser_pb2, geyser_pb2_grpc
from monitoring.base_listener import BaseTokenListener
from monitoring.geyser_event_processor import GeyserEventProcessor
from trading.base import TokenInfo
from utils.logger import get_logger
logger = get_logger(__name__)
class GeyserListener(BaseTokenListener):
"""Geyser listener for pump.fun token creation events."""
def __init__(self, geyser_endpoint: str, geyser_api_token: str, pump_program: Pubkey):
"""Initialize token listener.
Args:
geyser_endpoint: Geyser gRPC endpoint URL
geyser_api_token: API token for authentication
pump_program: Pump.fun program address
"""
self.geyser_endpoint = geyser_endpoint
self.geyser_api_token = geyser_api_token
self.pump_program = pump_program
self.event_processor = GeyserEventProcessor(pump_program)
async def _create_geyser_connection(self):
"""Establish a secure connection to the Geyser endpoint."""
auth = grpc.metadata_call_credentials(
lambda context, callback: callback(
(("authorization", f"Basic {self.geyser_api_token}"),), None
)
)
creds = grpc.composite_channel_credentials(
grpc.ssl_channel_credentials(), auth
)
channel = grpc.aio.secure_channel(self.geyser_endpoint, creds)
return geyser_pb2_grpc.GeyserStub(channel), channel
def _create_subscription_request(self):
"""Create a subscription request for Pump.fun transactions."""
request = geyser_pb2.SubscribeRequest()
request.transactions["pump_filter"].account_include.append(str(self.pump_program))
request.transactions["pump_filter"].failed = False
request.commitment = geyser_pb2.CommitmentLevel.PROCESSED
return request
async def listen_for_tokens(
self,
token_callback: Callable[[TokenInfo], Awaitable[None]],
match_string: str | None = None,
creator_address: str | None = None,
) -> None:
"""Listen for new token creations using Geyser subscription.
Args:
token_callback: Callback function for new tokens
match_string: Optional string to match in token name/symbol
creator_address: Optional creator address to filter by
"""
while True:
try:
stub, channel = await self._create_geyser_connection()
request = self._create_subscription_request()
logger.info(f"Connected to Geyser endpoint: {self.geyser_endpoint}")
logger.info(f"Monitoring for transactions involving program: {self.pump_program}")
try:
async for update in stub.Subscribe(iter([request])):
token_info = await self._process_update(update)
if not token_info:
continue
logger.info(
f"New token detected: {token_info.name} ({token_info.symbol})"
)
if match_string and not (
match_string.lower() in token_info.name.lower()
or match_string.lower() in token_info.symbol.lower()
):
logger.info(
f"Token does not match filter '{match_string}'. Skipping..."
)
continue
if (
creator_address
and str(token_info.user) != creator_address
):
logger.info(
f"Token not created by {creator_address}. Skipping..."
)
continue
await token_callback(token_info)
except grpc.aio.AioRpcError as e:
logger.error(f"gRPC error: {e.details()}")
await asyncio.sleep(5)
finally:
await channel.close()
except Exception as e:
logger.error(f"Geyser connection error: {e}")
logger.info("Reconnecting in 10 seconds...")
await asyncio.sleep(10)
async def _process_update(self, update) -> TokenInfo | None:
"""Process a Geyser update and extract token creation info.
Args:
update: Geyser update from the subscription
Returns:
TokenInfo if a token creation is found, None otherwise
"""
try:
if not update.HasField("transaction"):
return None
tx = update.transaction.transaction.transaction
msg = getattr(tx, "message", None)
if msg is None:
return None
for ix in msg.instructions:
# Skip non-Pump.fun program instructions
program_idx = ix.program_id_index
if program_idx >= len(msg.account_keys):
continue
program_id = msg.account_keys[program_idx]
if bytes(program_id) != bytes(self.pump_program):
continue
# Process instruction data
token_info = self.event_processor.process_transaction_data(
ix.data, ix.accounts, msg.account_keys
)
if token_info:
return token_info
return None
except Exception as e:
logger.error(f"Error processing Geyser update: {e}")
return None
+151
View File
@@ -0,0 +1,151 @@
"""
Event processing for pump.fun tokens using logsSubscribe data.
"""
import base64
import struct
from typing import Final
import base58
from solders.pubkey import Pubkey
from core.pubkeys import SystemAddresses
from trading.base import TokenInfo
from utils.logger import get_logger
logger = get_logger(__name__)
class LogsEventProcessor:
"""Processes events from pump.fun program logs."""
# Discriminator for create instruction to avoid non-create transactions
CREATE_DISCRIMINATOR: Final[int] = 8530921459188068891
def __init__(self, pump_program: Pubkey):
"""Initialize event processor.
Args:
pump_program: Pump.fun program address
"""
self.pump_program = pump_program
def process_program_logs(self, logs: list[str], signature: str) -> TokenInfo | None:
"""Process program logs and extract token info.
Args:
logs: List of log strings from the notification
signature: Transaction signature
Returns:
TokenInfo if a token creation is found, None otherwise
"""
# Check if this is a token creation
if not any("Program log: Instruction: Create" in log for log in logs):
return None
# Skip swaps as the first condition may pass them
if any("Program log: Instruction: CreateTokenAccount" in log for log in logs):
return None
# Find and process program data
for log in logs:
if "Program data:" in log:
try:
encoded_data = log.split(": ")[1]
decoded_data = base64.b64decode(encoded_data)
parsed_data = self._parse_create_instruction(decoded_data)
if parsed_data and "name" in parsed_data:
mint = Pubkey.from_string(parsed_data["mint"])
bonding_curve = Pubkey.from_string(parsed_data["bondingCurve"])
associated_curve = self._find_associated_bonding_curve(
mint, bonding_curve
)
return TokenInfo(
name=parsed_data["name"],
symbol=parsed_data["symbol"],
uri=parsed_data["uri"],
mint=mint,
bonding_curve=bonding_curve,
associated_bonding_curve=associated_curve,
user=Pubkey.from_string(parsed_data["user"]),
)
except Exception as e:
logger.error(f"Failed to process log data: {e}")
return None
def _parse_create_instruction(self, data: bytes) -> dict | None:
"""Parse the create instruction data.
Args:
data: Raw instruction data
Returns:
Dictionary of parsed data or None if parsing fails
"""
if len(data) < 8:
return None
# Check for the correct instruction discriminator
discriminator = struct.unpack("<Q", data[:8])[0]
if discriminator != self.CREATE_DISCRIMINATOR:
logger.info(f"Skipping non-Create instruction with discriminator: {discriminator}")
return None
offset = 8
parsed_data = {}
# Parse fields based on CreateEvent structure
fields = [
("name", "string"),
("symbol", "string"),
("uri", "string"),
("mint", "publicKey"),
("bondingCurve", "publicKey"),
("user", "publicKey"),
]
try:
for field_name, field_type in fields:
if field_type == "string":
length = struct.unpack("<I", data[offset : offset + 4])[0]
offset += 4
value = data[offset : offset + length].decode("utf-8")
offset += length
elif field_type == "publicKey":
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
offset += 32
parsed_data[field_name] = value
return parsed_data
except Exception as e:
logger.error(f"Failed to parse create instruction: {e}")
return None
def _find_associated_bonding_curve(
self, mint: Pubkey, bonding_curve: Pubkey
) -> Pubkey:
"""
Find the associated bonding curve for a given mint and bonding curve.
This uses the standard ATA derivation.
Args:
mint: Token mint address
bonding_curve: Bonding curve address
Returns:
Associated bonding curve address
"""
derived_address, _ = Pubkey.find_program_address(
[
bytes(bonding_curve),
bytes(SystemAddresses.TOKEN_PROGRAM),
bytes(mint),
],
SystemAddresses.ASSOCIATED_TOKEN_PROGRAM,
)
return derived_address
+167
View File
@@ -0,0 +1,167 @@
"""
WebSocket monitoring for pump.fun tokens using logsSubscribe.
"""
import asyncio
import json
from collections.abc import Awaitable, Callable
import websockets
from solders.pubkey import Pubkey
from monitoring.base_listener import BaseTokenListener
from monitoring.logs_event_processor import LogsEventProcessor
from trading.base import TokenInfo
from utils.logger import get_logger
logger = get_logger(__name__)
class LogsListener(BaseTokenListener):
"""WebSocket listener for pump.fun token creation events using logsSubscribe."""
def __init__(self, wss_endpoint: str, pump_program: Pubkey):
"""Initialize token listener.
Args:
wss_endpoint: WebSocket endpoint URL
pump_program: Pump.fun program address
"""
self.wss_endpoint = wss_endpoint
self.pump_program = pump_program
self.event_processor = LogsEventProcessor(pump_program)
self.ping_interval = 20 # seconds
async def listen_for_tokens(
self,
token_callback: Callable[[TokenInfo], Awaitable[None]],
match_string: str | None = None,
creator_address: str | None = None,
) -> None:
"""Listen for new token creations using logsSubscribe.
Args:
token_callback: Callback function for new tokens
match_string: Optional string to match in token name/symbol
creator_address: Optional creator address to filter by
"""
while True:
try:
async with websockets.connect(self.wss_endpoint) as websocket:
await self._subscribe_to_logs(websocket)
ping_task = asyncio.create_task(self._ping_loop(websocket))
try:
while True:
token_info = await self._wait_for_token_creation(websocket)
if not token_info:
continue
logger.info(
f"New token detected: {token_info.name} ({token_info.symbol})"
)
if match_string and not (
match_string.lower() in token_info.name.lower()
or match_string.lower() in token_info.symbol.lower()
):
logger.info(
f"Token does not match filter '{match_string}'. Skipping..."
)
continue
if (
creator_address
and str(token_info.user) != creator_address
):
logger.info(
f"Token not created by {creator_address}. Skipping..."
)
continue
await token_callback(token_info)
except websockets.exceptions.ConnectionClosed:
logger.warning("WebSocket connection closed. Reconnecting...")
ping_task.cancel()
except Exception as e:
logger.error(f"WebSocket connection error: {str(e)}")
logger.info("Reconnecting in 5 seconds...")
await asyncio.sleep(5)
async def _subscribe_to_logs(self, websocket) -> None:
"""Subscribe to logs mentioning the pump.fun program.
Args:
websocket: Active WebSocket connection
"""
subscription_message = json.dumps(
{
"jsonrpc": "2.0",
"id": 1,
"method": "logsSubscribe",
"params": [
{"mentions": [str(self.pump_program)]},
{"commitment": "processed"},
],
}
)
await websocket.send(subscription_message)
logger.info(f"Subscribed to logs mentioning program: {self.pump_program}")
# Wait for subscription confirmation
response = await websocket.recv()
response_data = json.loads(response)
if "result" in response_data:
logger.info(f"Subscription confirmed with ID: {response_data['result']}")
else:
logger.warning(f"Unexpected subscription response: {response}")
async def _ping_loop(self, websocket) -> None:
"""Keep connection alive with pings.
Args:
websocket: Active WebSocket connection
"""
try:
while True:
await asyncio.sleep(self.ping_interval)
try:
pong_waiter = await websocket.ping()
await asyncio.wait_for(pong_waiter, timeout=10)
except asyncio.TimeoutError:
logger.warning("Ping timeout - server not responding")
# Force reconnection
await websocket.close()
return
except asyncio.CancelledError:
pass
except Exception as e:
logger.error(f"Ping error: {str(e)}")
async def _wait_for_token_creation(self, websocket) -> TokenInfo | None:
try:
response = await asyncio.wait_for(websocket.recv(), timeout=30)
data = json.loads(response)
if "method" not in data or data["method"] != "logsNotification":
return None
log_data = data["params"]["result"]["value"]
logs = log_data.get("logs", [])
signature = log_data.get("signature", "unknown")
# Use the processor to extract token info
return self.event_processor.process_program_logs(logs, signature)
except asyncio.TimeoutError:
logger.debug("No data received for 30 seconds")
except websockets.exceptions.ConnectionClosed:
logger.warning("WebSocket connection closed")
raise
except Exception as e:
logger.error(f"Error processing WebSocket message: {str(e)}")
return None
View File
+101
View File
@@ -0,0 +1,101 @@
"""
Base interfaces for trading operations.
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any
from solders.pubkey import Pubkey
from core.pubkeys import PumpAddresses
@dataclass
class TokenInfo:
"""Token information."""
name: str
symbol: str
uri: str
mint: Pubkey
bonding_curve: Pubkey
associated_bonding_curve: Pubkey
user: Pubkey
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "TokenInfo":
"""Create TokenInfo from dictionary.
Args:
data: Dictionary with token data
Returns:
TokenInfo instance
"""
return cls(
name=data["name"],
symbol=data["symbol"],
uri=data["uri"],
mint=Pubkey.from_string(data["mint"]),
bonding_curve=Pubkey.from_string(data["bondingCurve"]),
associated_bonding_curve=Pubkey.from_string(data["associatedBondingCurve"]),
user=Pubkey.from_string(data["user"]),
)
def to_dict(self) -> dict[str, str]:
"""Convert to dictionary.
Returns:
Dictionary representation
"""
return {
"name": self.name,
"symbol": self.symbol,
"uri": self.uri,
"mint": str(self.mint),
"bondingCurve": str(self.bonding_curve),
"associatedBondingCurve": str(self.associated_bonding_curve),
"user": str(self.user),
}
@dataclass
class TradeResult:
"""Result of a trading operation."""
success: bool
tx_signature: str | None = None
error_message: str | None = None
amount: float | None = None
price: float | None = None
class Trader(ABC):
"""Base interface for trading operations."""
@abstractmethod
async def execute(self, *args, **kwargs) -> TradeResult:
"""Execute trading operation.
Returns:
TradeResult with operation outcome
"""
pass
def _get_relevant_accounts(self, token_info: TokenInfo) -> list[Pubkey]:
"""
Get the list of accounts relevant for calculating the priority fee.
Args:
token_info: Token information for the buy/sell operation.
Returns:
list[Pubkey]: List of relevant accounts.
"""
return [
token_info.mint, # Token mint address
token_info.bonding_curve, # Bonding curve address
PumpAddresses.PROGRAM, # Pump.fun program address
PumpAddresses.FEE, # Pump.fun fee account
]
+218
View File
@@ -0,0 +1,218 @@
"""
Buy operations for pump.fun tokens.
"""
import struct
from typing import Final
from solders.instruction import AccountMeta, Instruction
from solders.pubkey import Pubkey
from spl.token.instructions import create_idempotent_associated_token_account
from core.client import SolanaClient
from core.curve import BondingCurveManager
from core.priority_fee.manager import PriorityFeeManager
from core.pubkeys import (
LAMPORTS_PER_SOL,
TOKEN_DECIMALS,
PumpAddresses,
SystemAddresses,
)
from core.wallet import Wallet
from trading.base import TokenInfo, Trader, TradeResult
from utils.logger import get_logger
logger = get_logger(__name__)
# Discriminator for the buy instruction
EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack("<Q", 16927863322537952870)
class TokenBuyer(Trader):
"""Handles buying tokens on pump.fun."""
def __init__(
self,
client: SolanaClient,
wallet: Wallet,
curve_manager: BondingCurveManager,
priority_fee_manager: PriorityFeeManager,
amount: float,
slippage: float = 0.01,
max_retries: int = 5,
extreme_fast_token_amount: int = 0,
extreme_fast_mode: bool = False,
):
"""Initialize token buyer.
Args:
client: Solana client for RPC calls
wallet: Wallet for signing transactions
curve_manager: Bonding curve manager
amount: Amount of SOL to spend
slippage: Slippage tolerance (0.01 = 1%)
max_retries: Maximum number of retry attempts
extreme_fast_token_amount: Amount of token to buy if extreme fast mode is enabled
extreme_fast_mode: If enabled, avoid fetching associated bonding curve state
"""
self.client = client
self.wallet = wallet
self.curve_manager = curve_manager
self.priority_fee_manager = priority_fee_manager
self.amount = amount
self.slippage = slippage
self.max_retries = max_retries
self.extreme_fast_mode = extreme_fast_mode
self.extreme_fast_token_amount = extreme_fast_token_amount
async def execute(self, token_info: TokenInfo, *args, **kwargs) -> TradeResult:
"""Execute buy operation.
Args:
token_info: Token information
Returns:
TradeResult with buy outcome
"""
try:
# Convert amount to lamports
amount_lamports = int(self.amount * LAMPORTS_PER_SOL)
if self.extreme_fast_mode:
# Skip the wait and directly calculate the amount
token_amount = self.extreme_fast_token_amount
token_price_sol = self.amount / token_amount
#logger.info(f"EXTREME FAST Mode: Buying {token_amount} tokens.")
else:
# Regular behavior with RPC call
curve_state = await self.curve_manager.get_curve_state(token_info.bonding_curve)
token_price_sol = curve_state.calculate_price()
token_amount = self.amount / token_price_sol
# Calculate maximum SOL to spend with slippage
max_amount_lamports = int(amount_lamports * (1 + self.slippage))
associated_token_account = self.wallet.get_associated_token_address(
token_info.mint
)
tx_signature = await self._send_buy_transaction(
token_info,
associated_token_account,
token_amount,
max_amount_lamports,
)
logger.info(
f"Buying {token_amount:.6f} tokens at {token_price_sol:.8f} SOL per token"
)
logger.info(
f"Total cost: {self.amount:.6f} SOL (max: {max_amount_lamports / LAMPORTS_PER_SOL:.6f} SOL)"
)
success = await self.client.confirm_transaction(tx_signature)
if success:
logger.info(f"Buy transaction confirmed: {tx_signature}")
return TradeResult(
success=True,
tx_signature=tx_signature,
amount=token_amount,
price=token_price_sol,
)
else:
return TradeResult(
success=False,
error_message=f"Transaction failed to confirm: {tx_signature}",
)
except Exception as e:
logger.error(f"Buy operation failed: {e!s}")
return TradeResult(success=False, error_message=str(e))
async def _send_buy_transaction(
self,
token_info: TokenInfo,
associated_token_account: Pubkey,
token_amount: float,
max_amount_lamports: int,
) -> str:
"""Send buy transaction.
Args:
token_info: Token information
associated_token_account: User's token account
token_amount: Amount of tokens to buy
max_amount_lamports: Maximum SOL to spend in lamports
Returns:
Transaction signature
Raises:
Exception: If transaction fails after all retries
"""
accounts = [
AccountMeta(
pubkey=PumpAddresses.GLOBAL, is_signer=False, is_writable=False
),
AccountMeta(pubkey=PumpAddresses.FEE, is_signer=False, is_writable=True),
AccountMeta(pubkey=token_info.mint, is_signer=False, is_writable=False),
AccountMeta(
pubkey=token_info.bonding_curve, is_signer=False, is_writable=True
),
AccountMeta(
pubkey=token_info.associated_bonding_curve,
is_signer=False,
is_writable=True,
),
AccountMeta(
pubkey=associated_token_account, is_signer=False, is_writable=True
),
AccountMeta(pubkey=self.wallet.pubkey, is_signer=True, is_writable=True),
AccountMeta(
pubkey=SystemAddresses.PROGRAM, is_signer=False, is_writable=False
),
AccountMeta(
pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False
),
AccountMeta(
pubkey=SystemAddresses.RENT, is_signer=False, is_writable=False
),
AccountMeta(
pubkey=PumpAddresses.EVENT_AUTHORITY, is_signer=False, is_writable=False
),
AccountMeta(
pubkey=PumpAddresses.PROGRAM, is_signer=False, is_writable=False
),
]
# Prepare idempotent create ATA instruction: it will not fail if ATA already exists
idempotent_ata_ix = create_idempotent_associated_token_account(
self.wallet.pubkey,
self.wallet.pubkey,
token_info.mint,
SystemAddresses.TOKEN_PROGRAM
)
# Prepare buy instruction data
token_amount_raw = int(token_amount * 10**TOKEN_DECIMALS)
data = (
EXPECTED_DISCRIMINATOR
+ struct.pack("<Q", token_amount_raw)
+ struct.pack("<Q", max_amount_lamports)
)
buy_ix = Instruction(PumpAddresses.PROGRAM, data, accounts)
try:
return await self.client.build_and_send_transaction(
[idempotent_ata_ix, buy_ix],
self.wallet.keypair,
skip_preflight=True,
max_retries=self.max_retries,
priority_fee=await self.priority_fee_manager.calculate_priority_fee(
self._get_relevant_accounts(token_info)
),
)
except Exception as e:
logger.error(f"Buy transaction failed: {e!s}")
raise
+213
View File
@@ -0,0 +1,213 @@
"""
Sell operations for pump.fun tokens.
"""
import struct
from typing import Final
from solders.instruction import AccountMeta, Instruction
from solders.pubkey import Pubkey
from core.client import SolanaClient
from core.curve import BondingCurveManager
from core.priority_fee.manager import PriorityFeeManager
from core.pubkeys import (
LAMPORTS_PER_SOL,
TOKEN_DECIMALS,
PumpAddresses,
SystemAddresses,
)
from core.wallet import Wallet
from trading.base import TokenInfo, Trader, TradeResult
from utils.logger import get_logger
logger = get_logger(__name__)
# Discriminator for the sell instruction
EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack("<Q", 12502976635542562355)
class TokenSeller(Trader):
"""Handles selling tokens on pump.fun."""
def __init__(
self,
client: SolanaClient,
wallet: Wallet,
curve_manager: BondingCurveManager,
priority_fee_manager: PriorityFeeManager,
slippage: float = 0.25,
max_retries: int = 5,
):
"""Initialize token seller.
Args:
client: Solana client for RPC calls
wallet: Wallet for signing transactions
curve_manager: Bonding curve manager
slippage: Slippage tolerance (0.25 = 25%)
max_retries: Maximum number of retry attempts
"""
self.client = client
self.wallet = wallet
self.curve_manager = curve_manager
self.priority_fee_manager = priority_fee_manager
self.slippage = slippage
self.max_retries = max_retries
async def execute(self, token_info: TokenInfo, *args, **kwargs) -> TradeResult:
"""Execute sell operation.
Args:
token_info: Token information
Returns:
TradeResult with sell outcome
"""
try:
# Get associated token account
associated_token_account = self.wallet.get_associated_token_address(
token_info.mint
)
# Get token balance
token_balance = await self.client.get_token_account_balance(
associated_token_account
)
token_balance_decimal = token_balance / 10**TOKEN_DECIMALS
logger.info(f"Token balance: {token_balance_decimal}")
if token_balance == 0:
logger.info("No tokens to sell.")
return TradeResult(success=False, error_message="No tokens to sell")
# Fetch token price
curve_state = await self.curve_manager.get_curve_state(
token_info.bonding_curve
)
token_price_sol = curve_state.calculate_price()
logger.info(f"Price per Token: {token_price_sol:.8f} SOL")
# Calculate minimum SOL output with slippage
amount = token_balance
expected_sol_output = float(token_balance_decimal) * float(token_price_sol)
slippage_factor = 1 - self.slippage
min_sol_output = int(
(expected_sol_output * slippage_factor) * LAMPORTS_PER_SOL
)
logger.info(f"Selling {token_balance_decimal} tokens")
logger.info(f"Expected SOL output: {expected_sol_output:.8f} SOL")
logger.info(
f"Minimum SOL output (with {self.slippage * 100}% slippage): {min_sol_output / LAMPORTS_PER_SOL:.8f} SOL"
)
tx_signature = await self._send_sell_transaction(
token_info,
associated_token_account,
amount,
min_sol_output,
)
success = await self.client.confirm_transaction(tx_signature)
if success:
logger.info(f"Sell transaction confirmed: {tx_signature}")
return TradeResult(
success=True,
tx_signature=tx_signature,
amount=token_balance_decimal,
price=token_price_sol,
)
else:
return TradeResult(
success=False,
error_message=f"Transaction failed to confirm: {tx_signature}",
)
except Exception as e:
logger.error(f"Sell operation failed: {str(e)}")
return TradeResult(success=False, error_message=str(e))
async def _send_sell_transaction(
self,
token_info: TokenInfo,
associated_token_account: Pubkey,
token_amount: int,
min_sol_output: int,
) -> str:
"""Send sell transaction.
Args:
mint: Token information
associated_token_account: User's token account
token_amount: Amount of tokens to sell in raw units
min_sol_output: Minimum SOL to receive in lamports
Returns:
Transaction signature
Raises:
Exception: If transaction fails after all retries
"""
# Prepare sell instruction accounts
accounts = [
AccountMeta(
pubkey=PumpAddresses.GLOBAL, is_signer=False, is_writable=False
),
AccountMeta(pubkey=PumpAddresses.FEE, is_signer=False, is_writable=True),
AccountMeta(pubkey=token_info.mint, is_signer=False, is_writable=False),
AccountMeta(
pubkey=token_info.bonding_curve, is_signer=False, is_writable=True
),
AccountMeta(
pubkey=token_info.associated_bonding_curve,
is_signer=False,
is_writable=True,
),
AccountMeta(
pubkey=associated_token_account, is_signer=False, is_writable=True
),
AccountMeta(pubkey=self.wallet.pubkey, is_signer=True, is_writable=True),
AccountMeta(
pubkey=SystemAddresses.PROGRAM, is_signer=False, is_writable=False
),
AccountMeta(
pubkey=SystemAddresses.ASSOCIATED_TOKEN_PROGRAM,
is_signer=False,
is_writable=False,
),
AccountMeta(
pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False
),
AccountMeta(
pubkey=PumpAddresses.EVENT_AUTHORITY, is_signer=False, is_writable=False
),
AccountMeta(
pubkey=PumpAddresses.PROGRAM, is_signer=False, is_writable=False
),
]
# Prepare sell instruction data
data = (
EXPECTED_DISCRIMINATOR
+ struct.pack("<Q", token_amount)
+ struct.pack("<Q", min_sol_output)
)
sell_ix = Instruction(PumpAddresses.PROGRAM, data, accounts)
try:
return await self.client.build_and_send_transaction(
[sell_ix],
self.wallet.keypair,
skip_preflight=True,
max_retries=self.max_retries,
priority_fee=await self.priority_fee_manager.calculate_priority_fee(
self._get_relevant_accounts(token_info)
),
)
except Exception as e:
logger.error(f"Sell transaction failed: {str(e)}")
raise
+550
View File
@@ -0,0 +1,550 @@
"""
Main trading coordinator for pump.fun tokens.
Refactored PumpTrader to only process fresh tokens from WebSocket.
"""
import asyncio
import json
import os
from datetime import datetime
from time import monotonic
import uvloop
from solders.pubkey import Pubkey
from cleanup.modes import (
handle_cleanup_after_failure,
handle_cleanup_after_sell,
handle_cleanup_post_session,
)
from core.client import SolanaClient
from core.curve import BondingCurveManager
from core.priority_fee.manager import PriorityFeeManager
from core.pubkeys import PumpAddresses
from core.wallet import Wallet
from monitoring.block_listener import BlockListener
from monitoring.geyser_listener import GeyserListener
from monitoring.logs_listener import LogsListener
from trading.base import TokenInfo, TradeResult
from trading.buyer import TokenBuyer
from trading.seller import TokenSeller
from utils.logger import get_logger
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
logger = get_logger(__name__)
class PumpTrader:
"""Coordinates trading operations for pump.fun tokens with focus on freshness."""
def __init__(
self,
rpc_endpoint: str,
wss_endpoint: str,
private_key: str,
buy_amount: float,
buy_slippage: float,
sell_slippage: float,
listener_type: str = "logs",
geyser_endpoint: str | None = None,
geyser_api_token: str | None = None,
extreme_fast_mode: bool = False,
extreme_fast_token_amount: int = 30,
# Priority fee configuration
enable_dynamic_priority_fee: bool = False,
enable_fixed_priority_fee: bool = True,
fixed_priority_fee: int = 200_000,
extra_priority_fee: float = 0.0,
hard_cap_prior_fee: int = 200_000,
# Retry and timeout settings
max_retries: int = 3,
wait_time_after_creation: int = 15, # here and further - seconds
wait_time_after_buy: int = 15,
wait_time_before_new_token: int = 15,
max_token_age: int | float = 0.001,
token_wait_timeout: int = 30,
# Cleanup settings
cleanup_mode: str = "disabled",
cleanup_force_close_with_burn: bool = False,
cleanup_with_priority_fee: bool = False,
# Trading filters
match_string: str | None = None,
bro_address: str | None = None,
marry_mode: bool = False,
yolo_mode: bool = False,
):
"""Initialize the pump trader.
Args:
rpc_endpoint: RPC endpoint URL
wss_endpoint: WebSocket endpoint URL
private_key: Wallet private key
buy_amount: Amount of SOL to spend on buys
buy_slippage: Slippage tolerance for buys
sell_slippage: Slippage tolerance for sells
listener_type: Type of listener to use ('logs', 'blocks', or 'geyser')
geyser_endpoint: Geyser endpoint URL (required for geyser listener)
geyser_api_token: Geyser API token (required for geyser listener)
extreme_fast_mode: Whether to enable extreme fast mode
extreme_fast_token_amount: Maximum token amount for extreme fast mode
enable_dynamic_priority_fee: Whether to enable dynamic priority fees
enable_fixed_priority_fee: Whether to enable fixed priority fees
fixed_priority_fee: Fixed priority fee amount
extra_priority_fee: Extra percentage for priority fees
hard_cap_prior_fee: Hard cap for priority fees
max_retries: Maximum number of retry attempts
wait_time_after_creation: Time to wait after token creation (seconds)
wait_time_after_buy: Time to wait after buying a token (seconds)
wait_time_before_new_token: Time to wait before processing a new token (seconds)
max_token_age: Maximum age of token to process (seconds)
token_wait_timeout: Timeout for waiting for a token in single-token mode (seconds)
cleanup_mode: Cleanup mode ("disabled", "auto", or "manual")
cleanup_force_close_with_burn: Whether to force close with burn during cleanup
cleanup_with_priority_fee: Whether to use priority fees during cleanup
match_string: Optional string to match in token name/symbol
bro_address: Optional creator address to filter by
marry_mode: If True, only buy tokens and skip selling
yolo_mode: If True, trade continuously
"""
self.solana_client = SolanaClient(rpc_endpoint)
self.wallet = Wallet(private_key)
self.curve_manager = BondingCurveManager(self.solana_client)
self.priority_fee_manager = PriorityFeeManager(
client=self.solana_client,
enable_dynamic_fee=enable_dynamic_priority_fee,
enable_fixed_fee=enable_fixed_priority_fee,
fixed_fee=fixed_priority_fee,
extra_fee=extra_priority_fee,
hard_cap=hard_cap_prior_fee,
)
self.buyer = TokenBuyer(
self.solana_client,
self.wallet,
self.curve_manager,
self.priority_fee_manager,
buy_amount,
buy_slippage,
max_retries,
extreme_fast_token_amount,
extreme_fast_mode
)
self.seller = TokenSeller(
self.solana_client,
self.wallet,
self.curve_manager,
self.priority_fee_manager,
sell_slippage,
max_retries,
)
# Initialize the appropriate listener type
listener_type = listener_type.lower()
if listener_type == "geyser":
if not geyser_endpoint or not geyser_api_token:
raise ValueError("Geyser endpoint and API token are required for geyser listener")
self.token_listener = GeyserListener(
geyser_endpoint,
geyser_api_token,
PumpAddresses.PROGRAM
)
logger.info("Using Geyser listener for token monitoring")
elif listener_type == "logs":
self.token_listener = LogsListener(wss_endpoint, PumpAddresses.PROGRAM)
logger.info("Using logsSubscribe listener for token monitoring")
else:
self.token_listener = BlockListener(wss_endpoint, PumpAddresses.PROGRAM)
logger.info("Using blockSubscribe listener for token monitoring")
# Trading parameters
self.buy_amount = buy_amount
self.buy_slippage = buy_slippage
self.sell_slippage = sell_slippage
self.max_retries = max_retries
self.extreme_fast_mode = extreme_fast_mode
self.extreme_fast_token_amount = extreme_fast_token_amount
# Timing parameters
self.wait_time_after_creation = wait_time_after_creation
self.wait_time_after_buy = wait_time_after_buy
self.wait_time_before_new_token = wait_time_before_new_token
self.max_token_age = max_token_age
self.token_wait_timeout = token_wait_timeout
# Cleanup parameters
self.cleanup_mode = cleanup_mode
self.cleanup_force_close_with_burn = cleanup_force_close_with_burn
self.cleanup_with_priority_fee = cleanup_with_priority_fee
# Trading filters/modes
self.match_string = match_string
self.bro_address = bro_address
self.marry_mode = marry_mode
self.yolo_mode = yolo_mode
# State tracking
self.traded_mints: set[Pubkey] = set()
self.token_queue: asyncio.Queue = asyncio.Queue()
self.processing: bool = False
self.processed_tokens: set[str] = set()
self.token_timestamps: dict[str, float] = {}
async def start(self) -> None:
"""Start the trading bot and listen for new tokens."""
logger.info("Starting pump.fun trader")
logger.info(f"Match filter: {self.match_string if self.match_string else 'None'}")
logger.info(f"Creator filter: {self.bro_address if self.bro_address else 'None'}")
logger.info(f"Marry mode: {self.marry_mode}")
logger.info(f"YOLO mode: {self.yolo_mode}")
logger.info(f"Max token age: {self.max_token_age} seconds")
try:
health_resp = await self.solana_client.get_health()
logger.info(f"RPC warm-up successful (getHealth passed: {health_resp})")
except Exception as e:
logger.warning(f"RPC warm-up failed: {e!s}")
try:
# Choose operating mode based on yolo_mode
if not self.yolo_mode:
# Single token mode: process one token and exit
logger.info("Running in single token mode - will process one token and exit")
token_info = await self._wait_for_token()
if token_info:
await self._handle_token(token_info)
logger.info("Finished processing single token. Exiting...")
else:
logger.info(f"No suitable token found within timeout period ({self.token_wait_timeout}s). Exiting...")
else:
# Continuous mode: process tokens until interrupted
logger.info("Running in continuous mode - will process tokens until interrupted")
processor_task = asyncio.create_task(
self._process_token_queue()
)
try:
await self.token_listener.listen_for_tokens(
lambda token: self._queue_token(token),
self.match_string,
self.bro_address,
)
except Exception as e:
logger.error(f"Token listening stopped due to error: {e!s}")
finally:
processor_task.cancel()
try:
await processor_task
except asyncio.CancelledError:
pass
except Exception as e:
logger.error(f"Trading stopped due to error: {e!s}")
finally:
await self._cleanup_resources()
logger.info("Pump trader has shut down")
async def _wait_for_token(self) -> TokenInfo | None:
"""Wait for a single token to be detected.
Returns:
TokenInfo or None if timeout occurs
"""
# Create a one-time event to signal when a token is found
token_found = asyncio.Event()
found_token = None
async def token_callback(token: TokenInfo) -> None:
nonlocal found_token
token_key = str(token.mint)
# Only process if not already processed and fresh
if token_key not in self.processed_tokens:
# Record when the token was discovered
self.token_timestamps[token_key] = monotonic()
found_token = token
self.processed_tokens.add(token_key)
token_found.set()
listener_task = asyncio.create_task(
self.token_listener.listen_for_tokens(
token_callback,
self.match_string,
self.bro_address,
)
)
# Wait for a token with a timeout
try:
logger.info(f"Waiting for a suitable token (timeout: {self.token_wait_timeout}s)...")
await asyncio.wait_for(token_found.wait(), timeout=self.token_wait_timeout)
logger.info(f"Found token: {found_token.symbol} ({found_token.mint})")
return found_token
except TimeoutError:
logger.info(f"Timed out after waiting {self.token_wait_timeout}s for a token")
return None
finally:
listener_task.cancel()
try:
await listener_task
except asyncio.CancelledError:
pass
async def _cleanup_resources(self) -> None:
"""Perform cleanup operations before shutting down."""
if self.traded_mints:
try:
logger.info(f"Cleaning up {len(self.traded_mints)} traded token(s)...")
await handle_cleanup_post_session(
self.solana_client,
self.wallet,
list(self.traded_mints),
self.priority_fee_manager,
self.cleanup_mode,
self.cleanup_with_priority_fee,
self.cleanup_force_close_with_burn
)
except Exception as e:
logger.error(f"Error during cleanup: {e!s}")
old_keys = {k for k in self.token_timestamps if k not in self.processed_tokens}
for key in old_keys:
self.token_timestamps.pop(key, None)
await self.solana_client.close()
async def _queue_token(
self, token_info: TokenInfo
) -> None:
"""Queue a token for processing if not already processed.
Args:
token_info: Token information to queue
"""
token_key = str(token_info.mint)
if token_key in self.processed_tokens:
logger.debug(f"Token {token_info.symbol} already processed. Skipping...")
return
# Record timestamp when token was discovered
self.token_timestamps[token_key] = monotonic()
await self.token_queue.put(token_info)
logger.info(f"Queued new token: {token_info.symbol} ({token_info.mint})")
async def _process_token_queue(self) -> None:
"""Continuously process tokens from the queue, only if they're fresh."""
while True:
try:
token_info = await self.token_queue.get()
token_key = str(token_info.mint)
# Check if token is still "fresh"
current_time = monotonic()
token_age = current_time - self.token_timestamps.get(
token_key, current_time
)
if token_age > self.max_token_age:
logger.info(
f"Skipping token {token_info.symbol} - too old ({token_age:.1f}s > {self.max_token_age}s)"
)
continue
self.processed_tokens.add(token_key)
logger.info(
f"Processing fresh token: {token_info.symbol} (age: {token_age:.1f}s)"
)
await self._handle_token(token_info)
except asyncio.CancelledError:
# Handle cancellation gracefully
logger.info("Token queue processor was cancelled")
break
except Exception as e:
logger.error(f"Error in token queue processor: {e!s}")
finally:
self.token_queue.task_done()
async def _handle_token(
self, token_info: TokenInfo
) -> None:
"""Handle a new token creation event.
Args:
token_info: Token information
"""
try:
# Wait for bonding curve to stabilize (unless in extreme fast mode)
if not self.extreme_fast_mode:
# Save token info to file
# await self._save_token_info(token_info)
logger.info(
f"Waiting for {self.wait_time_after_creation} seconds for the bonding curve to stabilize..."
)
await asyncio.sleep(self.wait_time_after_creation)
# Buy token
logger.info(
f"Buying {self.buy_amount:.6f} SOL worth of {token_info.symbol}..."
)
buy_result: TradeResult = await self.buyer.execute(token_info)
if buy_result.success:
await self._handle_successful_buy(token_info, buy_result)
else:
await self._handle_failed_buy(token_info, buy_result)
# Only wait for next token in yolo mode
if self.yolo_mode:
logger.info(
f"YOLO mode enabled. Waiting {self.wait_time_before_new_token} seconds before looking for next token..."
)
await asyncio.sleep(self.wait_time_before_new_token)
except Exception as e:
logger.error(f"Error handling token {token_info.symbol}: {e!s}")
async def _handle_successful_buy(
self, token_info: TokenInfo, buy_result: TradeResult
) -> None:
"""Handle successful token purchase.
Args:
token_info: Token information
buy_result: The result of the buy operation
"""
logger.info(f"Successfully bought {token_info.symbol}")
self._log_trade(
"buy",
token_info,
buy_result.price, # type: ignore
buy_result.amount, # type: ignore
buy_result.tx_signature,
)
self.traded_mints.add(token_info.mint)
# Sell token if not in marry mode
if not self.marry_mode:
logger.info(
f"Waiting for {self.wait_time_after_buy} seconds before selling..."
)
await asyncio.sleep(self.wait_time_after_buy)
logger.info(f"Selling {token_info.symbol}...")
sell_result: TradeResult = await self.seller.execute(token_info)
if sell_result.success:
logger.info(f"Successfully sold {token_info.symbol}")
self._log_trade(
"sell",
token_info,
sell_result.price, # type: ignore
sell_result.amount, # type: ignore
sell_result.tx_signature,
)
# Close ATA if enabled
await handle_cleanup_after_sell(
self.solana_client,
self.wallet,
token_info.mint,
self.priority_fee_manager,
self.cleanup_mode,
self.cleanup_with_priority_fee,
self.cleanup_force_close_with_burn
)
else:
logger.error(
f"Failed to sell {token_info.symbol}: {sell_result.error_message}"
)
else:
logger.info("Marry mode enabled. Skipping sell operation.")
async def _handle_failed_buy(
self, token_info: TokenInfo, buy_result: TradeResult
) -> None:
"""Handle failed token purchase.
Args:
token_info: Token information
buy_result: The result of the buy operation
"""
logger.error(
f"Failed to buy {token_info.symbol}: {buy_result.error_message}"
)
# Close ATA if enabled
await handle_cleanup_after_failure(
self.solana_client,
self.wallet,
token_info.mint,
self.priority_fee_manager,
self.cleanup_mode,
self.cleanup_with_priority_fee,
self.cleanup_force_close_with_burn
)
async def _save_token_info(
self, token_info: TokenInfo
) -> None:
"""Save token information to a file.
Args:
token_info: Token information
"""
try:
os.makedirs("trades", exist_ok=True)
file_name = os.path.join("trades", f"{token_info.mint}.txt")
with open(file_name, "w") as file:
file.write(json.dumps(token_info.to_dict(), indent=2))
logger.info(f"Token information saved to {file_name}")
except Exception as e:
logger.error(f"Failed to save token information: {e!s}")
def _log_trade(
self,
action: str,
token_info: TokenInfo,
price: float,
amount: float,
tx_hash: str | None,
) -> None:
"""Log trade information.
Args:
action: Trade action (buy/sell)
token_info: Token information
price: Token price in SOL
amount: Trade amount in SOL
tx_hash: Transaction hash
"""
try:
os.makedirs("trades", exist_ok=True)
log_entry = {
"timestamp": datetime.utcnow().isoformat(),
"action": action,
"token_address": str(token_info.mint),
"symbol": token_info.symbol,
"price": price,
"amount": amount,
"tx_hash": str(tx_hash) if tx_hash else None,
}
with open("trades/trades.log", "a") as log_file:
log_file.write(json.dumps(log_entry) + "\n")
except Exception as e:
logger.error(f"Failed to log trade information: {e!s}")
View File
+58
View File
@@ -0,0 +1,58 @@
"""
Logging utilities for the pump.fun trading bot.
"""
import logging
# Global dict to store loggers
_loggers: dict[str, logging.Logger] = {}
def get_logger(name: str, level: int = logging.INFO) -> logging.Logger:
"""Get or create a logger with the given name.
Args:
name: Logger name, typically __name__
level: Logging level
Returns:
Configured logger
"""
global _loggers
if name in _loggers:
return _loggers[name]
logger = logging.getLogger(name)
logger.setLevel(level)
_loggers[name] = logger
return logger
def setup_file_logging(
filename: str = "pump_trading.log", level: int = logging.INFO
) -> None:
"""Set up file logging for all loggers.
Args:
filename: Log file path
level: Logging level for file handler
"""
root_logger = logging.getLogger()
# Check if file handler with same filename already exists
for handler in root_logger.handlers:
if isinstance(handler, logging.FileHandler) and handler.baseFilename == filename:
return # File handler already added
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
file_handler = logging.FileHandler(filename)
file_handler.setLevel(level)
file_handler.setFormatter(formatter)
root_logger.addHandler(file_handler)
+208
View File
@@ -0,0 +1,208 @@
"""
Test script to compare BlockListener, LogsListener, and GeyserListener
Runs all listeners simultaneously to compare their performance
"""
import asyncio
import logging
import os
import sys
import time
from pathlib import Path
from dotenv import load_dotenv
sys.path.append(str(Path(__file__).parent.parent / "src"))
from core.pubkeys import PumpAddresses
from monitoring.block_listener import BlockListener
from monitoring.geyser_listener import GeyserListener
from monitoring.logs_listener import LogsListener
from trading.base import TokenInfo
load_dotenv()
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger("listener-comparison")
class TimingTokenCallback:
def __init__(self, name: str):
self.name = name
self.detected_tokens = []
self.detection_times = {}
async def on_token_created(self, token_info: TokenInfo) -> None:
"""Process detected token with timing information"""
token_key = str(token_info.mint)
detection_time = time.time()
self.detected_tokens.append(token_info)
self.detection_times[token_key] = detection_time
logger.info(f"[{self.name}] Detected: {token_info.name} ({token_info.symbol})")
print(f"\n{'=' * 50}")
print(f"[{self.name}] NEW TOKEN: {token_info.name}")
print(f"Symbol: {token_info.symbol}")
print(f"Mint: {token_info.mint}")
print(f"Detection time: {detection_time}")
print(f"{'=' * 50}\n")
async def listen_with_timeout(listener, callback, timeout):
"""Run a listener for a specified duration"""
try:
listen_task = asyncio.create_task(
listener.listen_for_tokens(callback.on_token_created)
)
await asyncio.sleep(timeout)
listen_task.cancel()
try:
await listen_task
except asyncio.CancelledError:
pass
except Exception as e:
logger.error(f"Error in listener {callback.name}: {e}")
async def run_comparison(test_duration: int = 300):
"""Run all listeners and compare their performance"""
wss_endpoint = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
geyser_endpoint = os.environ.get("GEYSER_ENDPOINT")
geyser_api_token = os.environ.get("GEYSER_API_TOKEN")
if not wss_endpoint:
logger.error("SOLANA_NODE_WSS_ENDPOINT environment variable is not set")
return
logger.info(f"Connecting to WebSocket: {wss_endpoint}")
block_listener = BlockListener(wss_endpoint, PumpAddresses.PROGRAM)
logs_listener = LogsListener(wss_endpoint, PumpAddresses.PROGRAM)
block_callback = TimingTokenCallback("BlockListener")
logs_callback = TimingTokenCallback("LogsListener")
listener_tasks = [
listen_with_timeout(block_listener, block_callback, test_duration),
listen_with_timeout(logs_listener, logs_callback, test_duration)
]
callbacks = [block_callback, logs_callback]
listener_names = ["BlockListener", "LogsListener"]
# Initialize Geyser listener if credentials are available
if geyser_endpoint and geyser_api_token:
logger.info(f"Connecting to Geyser API: {geyser_endpoint}")
geyser_listener = GeyserListener(geyser_endpoint, geyser_api_token, PumpAddresses.PROGRAM)
geyser_callback = TimingTokenCallback("GeyserListener")
listener_tasks.append(
listen_with_timeout(geyser_listener, geyser_callback, test_duration)
)
callbacks.append(geyser_callback)
listener_names.append("GeyserListener")
else:
logger.warning("Geyser API credentials not found. Running without Geyser listener.")
logger.info("Starting all listeners simultaneously...")
logger.info(f"Comparison running for {test_duration} seconds...")
try:
# Start all listeners at the same time
start_time = time.time()
await asyncio.gather(*listener_tasks)
end_time = time.time()
logger.info(f"Test completed in {end_time - start_time:.2f} seconds")
except KeyboardInterrupt:
logger.info("Test interrupted by user")
# No need for explicit cancellation as gather() will be interrupted
for i, callback in enumerate(callbacks):
logger.info(f"{listener_names[i]} detected {len(callback.detected_tokens)} tokens")
# Find tokens detected by multiple listeners
all_mints = {}
for i, callback in enumerate(callbacks):
mints = {str(token.mint) for token in callback.detected_tokens}
all_mints[listener_names[i]] = mints
# Analyze common detections between all listeners
if len(callbacks) > 1:
logger.info("\nAnalyzing token detection across listeners:")
# Find tokens detected by all listeners
if len(callbacks) > 2: # If we have all 3 listeners
common_to_all = set.intersection(*all_mints.values())
logger.info(f"Tokens detected by all listeners: {len(common_to_all)}")
# Compare pairs of listeners
listeners = list(all_mints.keys())
for i in range(len(listeners)):
for j in range(i+1, len(listeners)):
listener1 = listeners[i]
listener2 = listeners[j]
common = all_mints[listener1].intersection(all_mints[listener2])
logger.info(f"Tokens detected by both {listener1} and {listener2}: {len(common)}")
unique1 = all_mints[listener1] - all_mints[listener2]
unique2 = all_mints[listener2] - all_mints[listener1]
logger.info(f"Tokens unique to {listener1}: {len(unique1)}")
logger.info(f"Tokens unique to {listener2}: {len(unique2)}")
# Find tokens detected by at least one listener
all_detected = set.union(*all_mints.values())
logger.info(f"Total unique tokens detected by any listener: {len(all_detected)}")
logger.info("\nDetection speed comparison:")
# Collect all tokens detected by at least two listeners
detection_comparisons = []
for mint in set.union(*all_mints.values()):
detections = {}
for i, callback in enumerate(callbacks):
if mint in callback.detection_times:
detections[listener_names[i]] = callback.detection_times[mint]
if len(detections) > 1: # Only consider tokens detected by multiple listeners
detection_comparisons.append((mint, detections))
if detection_comparisons:
logger.info("Token | " + " | ".join(listener_names) + " | Fastest")
logger.info("-" * 80)
for mint, detections in detection_comparisons:
# Create row with detection times or "N/A" if not detected
times = []
for name in listener_names:
time_str = f"{detections.get(name, 0):.6f}" if name in detections else "N/A"
times.append(time_str)
# Determine fastest listener
valid_times = {name: time for name, time in detections.items() if time > 0}
fastest = min(valid_times.items(), key=lambda x: x[1])[0] if valid_times else "N/A"
logger.info(f"{mint[:10]}... | " + " | ".join(times) + f" | {fastest}")
else:
logger.info("No tokens were detected by multiple listeners for timing comparison")
if __name__ == "__main__":
test_duration = 30 # seconds
if len(sys.argv) > 1:
try:
test_duration = int(sys.argv[1])
except ValueError:
logger.error(f"Invalid test duration: {sys.argv[1]}. Using default of {test_duration} seconds.")
logger.info("Starting listener comparison test")
logger.info(f"Will run for {test_duration} seconds")
asyncio.run(run_comparison(test_duration))
+101
View File
@@ -0,0 +1,101 @@
"""
Test script for BlockListener
Tests websocket monitoring for new pump.fun tokens using blockSubscribe
"""
import asyncio
import logging
import os
import sys
from pathlib import Path
from dotenv import load_dotenv
sys.path.append(str(Path(__file__).parent.parent / "src"))
from core.pubkeys import PumpAddresses
from monitoring.block_listener import BlockListener
from trading.base import TokenInfo
load_dotenv()
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger("block-listener-test")
class TestTokenCallback:
def __init__(self):
self.detected_tokens = []
async def on_token_created(self, token_info: TokenInfo) -> None:
"""Process detected token"""
logger.info(f"New token detected: {token_info.name} ({token_info.symbol})")
logger.info(f"Mint: {token_info.mint}")
self.detected_tokens.append(token_info)
print(f"\n{'=' * 50}")
print(f"NEW TOKEN: {token_info.name}")
print(f"Symbol: {token_info.symbol}")
print(f"Mint: {token_info.mint}")
print(f"URI: {token_info.uri}")
print(f"Creator: {token_info.user}")
print(f"Bonding Curve: {token_info.bonding_curve}")
print(f"Associated Bonding Curve: {token_info.associated_bonding_curve}")
print(f"{'=' * 50}\n")
async def test_block_listener(
match_string: str | None = None,
creator_address: str | None = None,
test_duration: int = 60,
):
"""Test the block listener functionality"""
wss_endpoint = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
if not wss_endpoint:
logger.error("SOLANA_NODE_WSS_ENDPOINT environment variable is not set")
return []
logger.info(f"Connecting to WebSocket: {wss_endpoint}")
listener = BlockListener(wss_endpoint, PumpAddresses.PROGRAM)
callback = TestTokenCallback()
if match_string:
logger.info(f"Filtering tokens matching: {match_string}")
if creator_address:
logger.info(f"Filtering tokens by creator: {creator_address}")
listen_task = asyncio.create_task(
listener.listen_for_tokens(
callback.on_token_created,
match_string=match_string,
creator_address=creator_address,
)
)
logger.info(f"Listening for {test_duration} seconds...")
try:
await asyncio.sleep(test_duration)
except KeyboardInterrupt:
logger.info("Test interrupted by user")
finally:
listen_task.cancel()
try:
await listen_task
except asyncio.CancelledError:
pass
logger.info(f"Detected {len(callback.detected_tokens)} tokens")
for token in callback.detected_tokens:
logger.info(f" - {token.name} ({token.symbol}): {token.mint}")
return callback.detected_tokens
if __name__ == "__main__":
match_string = None # Update if you want to filter tokens by name/symbol
creator_address = None # Update if you want to filter tokens by creator address
test_duration = 30
logger.info("Starting block listener test (using blockSubscribe)")
asyncio.run(test_block_listener(match_string, creator_address, test_duration))
+107
View File
@@ -0,0 +1,107 @@
"""
Test script for GeyserListener
Tests gRPC monitoring for new pump.fun tokens using Geyser
"""
import asyncio
import logging
import os
import sys
from pathlib import Path
from dotenv import load_dotenv
sys.path.append(str(Path(__file__).parent.parent / "src"))
from core.pubkeys import PumpAddresses
from monitoring.geyser_listener import GeyserListener
from trading.base import TokenInfo
load_dotenv()
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger("geyser-listener-test")
class TestTokenCallback:
def __init__(self):
self.detected_tokens = []
async def on_token_created(self, token_info: TokenInfo) -> None:
"""Process detected token"""
logger.info(f"New token detected: {token_info.name} ({token_info.symbol})")
logger.info(f"Mint: {token_info.mint}")
self.detected_tokens.append(token_info)
print(f"\n{'=' * 50}")
print(f"NEW TOKEN: {token_info.name}")
print(f"Symbol: {token_info.symbol}")
print(f"Mint: {token_info.mint}")
print(f"URI: {token_info.uri}")
print(f"Creator: {token_info.user}")
print(f"Bonding Curve: {token_info.bonding_curve}")
print(f"Associated Bonding Curve: {token_info.associated_bonding_curve}")
print(f"{'=' * 50}\n")
async def test_geyser_listener(
match_string: str | None = None,
creator_address: str | None = None,
test_duration: int = 60,
):
"""Test the Geyser listener functionality"""
geyser_endpoint = os.environ.get("GEYSER_ENDPOINT")
geyser_api_token = os.environ.get("GEYSER_API_TOKEN")
if not geyser_endpoint:
logger.error("GEYSER_ENDPOINT environment variable is not set")
return []
if not geyser_api_token:
logger.error("GEYSER_API_TOKEN environment variable is not set")
return []
logger.info(f"Connecting to Geyser API: {geyser_endpoint}")
listener = GeyserListener(geyser_endpoint, geyser_api_token, PumpAddresses.PROGRAM)
callback = TestTokenCallback()
if match_string:
logger.info(f"Filtering tokens matching: {match_string}")
if creator_address:
logger.info(f"Filtering tokens by creator: {creator_address}")
listen_task = asyncio.create_task(
listener.listen_for_tokens(
callback.on_token_created,
match_string=match_string,
creator_address=creator_address,
)
)
logger.info(f"Listening for {test_duration} seconds...")
try:
await asyncio.sleep(test_duration)
except KeyboardInterrupt:
logger.info("Test interrupted by user")
finally:
listen_task.cancel()
try:
await listen_task
except asyncio.CancelledError:
pass
logger.info(f"Detected {len(callback.detected_tokens)} tokens")
for token in callback.detected_tokens:
logger.info(f" - {token.name} ({token.symbol}): {token.mint}")
return callback.detected_tokens
if __name__ == "__main__":
match_string = None # Update if you want to filter tokens by name/symbol
creator_address = None # Update if you want to filter tokens by creator address
test_duration = 30
logger.info("Starting Geyser listener test (using Geyser API)")
asyncio.run(test_geyser_listener(match_string, creator_address, test_duration))
+101
View File
@@ -0,0 +1,101 @@
"""
Test script for LogsListener
Tests websocket monitoring for new pump.fun tokens using logsSubscribe
"""
import asyncio
import logging
import os
import sys
from pathlib import Path
from dotenv import load_dotenv
sys.path.append(str(Path(__file__).parent.parent / "src"))
from core.pubkeys import PumpAddresses
from monitoring.logs_listener import LogsListener
from trading.base import TokenInfo
load_dotenv()
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger("logs-listener-test")
class TestTokenCallback:
def __init__(self):
self.detected_tokens = []
async def on_token_created(self, token_info: TokenInfo) -> None:
"""Process detected token"""
logger.info(f"New token detected: {token_info.name} ({token_info.symbol})")
logger.info(f"Mint: {token_info.mint}")
self.detected_tokens.append(token_info)
print(f"\n{'=' * 50}")
print(f"NEW TOKEN: {token_info.name}")
print(f"Symbol: {token_info.symbol}")
print(f"Mint: {token_info.mint}")
print(f"URI: {token_info.uri}")
print(f"Creator: {token_info.user}")
print(f"Bonding Curve: {token_info.bonding_curve}")
print(f"Associated Bonding Curve: {token_info.associated_bonding_curve}")
print(f"{'=' * 50}\n")
async def test_logs_listener(
match_string: str | None = None,
creator_address: str | None = None,
test_duration: int = 60,
):
"""Test the logs listener functionality"""
wss_endpoint = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
if not wss_endpoint:
logger.error("SOLANA_NODE_WSS_ENDPOINT environment variable is not set")
return []
logger.info(f"Connecting to WebSocket: {wss_endpoint}")
listener = LogsListener(wss_endpoint, PumpAddresses.PROGRAM)
callback = TestTokenCallback()
if match_string:
logger.info(f"Filtering tokens matching: {match_string}")
if creator_address:
logger.info(f"Filtering tokens by creator: {creator_address}")
listen_task = asyncio.create_task(
listener.listen_for_tokens(
callback.on_token_created,
match_string=match_string,
creator_address=creator_address,
)
)
logger.info(f"Listening for {test_duration} seconds...")
try:
await asyncio.sleep(test_duration)
except KeyboardInterrupt:
logger.info("Test interrupted by user")
finally:
listen_task.cancel()
try:
await listen_task
except asyncio.CancelledError:
pass
logger.info(f"Detected {len(callback.detected_tokens)} tokens")
for token in callback.detected_tokens:
logger.info(f" - {token.name} ({token.symbol}): {token.mint}")
return callback.detected_tokens
if __name__ == "__main__":
match_string = None # Update if you want to filter tokens by name/symbol
creator_address = None # Update if you want to filter tokens by creator address
test_duration = 30
logger.info("Starting logs listener test (using logsSubscribe)")
asyncio.run(test_logs_listener(match_string, creator_address, test_duration))
-157
View File
@@ -1,157 +0,0 @@
import asyncio
import json
import base64
import struct
import base58
import hashlib
import websockets
import os
import argparse
from datetime import datetime
from solana.rpc.async_api import AsyncClient
from solana.transaction import Transaction
from solana.rpc.commitment import Confirmed
from solana.rpc.types import TxOpts
from solders.pubkey import Pubkey
from solders.keypair import Keypair
from solders.instruction import Instruction, AccountMeta
from solders.system_program import TransferParams, transfer
from solders.transaction import VersionedTransaction
from spl.token.instructions import get_associated_token_address
import spl.token.instructions as spl_token
from config import *
# Import functions from buy.py
from buy import get_pump_curve_state, calculate_pump_curve_price, buy_token, listen_for_create_transaction
# Import functions from sell.py
from sell import sell_token
def log_trade(action, token_data, price, tx_hash):
os.makedirs("trades", exist_ok=True)
log_entry = {
"timestamp": datetime.utcnow().isoformat(),
"action": action,
"token_address": token_data['mint'],
"price": price,
"tx_hash": tx_hash
}
with open("trades/trades.log", 'a') as log_file:
json.dump(log_entry, log_file)
log_file.write("\n")
async def trade(websocket=None, match_string=None, bro_address=None, marry_mode=False, yolo_mode=False):
if websocket is None:
async with websockets.connect(WSS_ENDPOINT) as websocket:
await _trade(websocket, match_string, bro_address, marry_mode, yolo_mode)
else:
await _trade(websocket, match_string, bro_address, marry_mode, yolo_mode)
async def _trade(websocket, match_string=None, bro_address=None, marry_mode=False, yolo_mode=False):
while True:
print("Waiting for a new token creation...")
token_data = await listen_for_create_transaction(websocket)
print("New token created:")
print(json.dumps(token_data, indent=2))
if match_string and not (match_string.lower() in token_data['name'].lower() or match_string.lower() in token_data['symbol'].lower()):
print(f"Token does not match the criteria '{match_string}'. Skipping...")
if not yolo_mode:
break
continue
if bro_address and token_data['user'] != bro_address:
print(f"Token not created by the specified user '{bro_address}'. Skipping...")
if not yolo_mode:
break
continue
# Save token information to a .txt file in the "trades" directory
mint_address = token_data['mint']
os.makedirs("trades", exist_ok=True)
file_name = os.path.join("trades", f"{mint_address}.txt")
with open(file_name, 'w') as file:
file.write(json.dumps(token_data, indent=2))
print(f"Token information saved to {file_name}")
print("Waiting for 15 seconds for things to stabilize...")
await asyncio.sleep(15)
mint = Pubkey.from_string(token_data['mint'])
bonding_curve = Pubkey.from_string(token_data['bondingCurve'])
associated_bonding_curve = Pubkey.from_string(token_data['associatedBondingCurve'])
# Fetch the token price
async with AsyncClient(RPC_ENDPOINT) as client:
curve_state = await get_pump_curve_state(client, bonding_curve)
token_price_sol = calculate_pump_curve_price(curve_state)
print(f"Bonding curve address: {bonding_curve}")
print(f"Token price: {token_price_sol:.10f} SOL")
print(f"Buying {BUY_AMOUNT:.6f} SOL worth of the new token with {BUY_SLIPPAGE*100:.1f}% slippage tolerance...")
buy_tx_hash = await buy_token(mint, bonding_curve, associated_bonding_curve, BUY_AMOUNT, BUY_SLIPPAGE)
if buy_tx_hash:
log_trade("buy", token_data, token_price_sol, str(buy_tx_hash))
else:
print("Buy transaction failed.")
if not marry_mode:
print("Waiting for 20 seconds before selling...")
await asyncio.sleep(20)
print(f"Selling tokens with {SELL_SLIPPAGE*100:.1f}% slippage tolerance...")
sell_tx_hash = await sell_token(mint, bonding_curve, associated_bonding_curve, SELL_SLIPPAGE)
if sell_tx_hash:
log_trade("sell", token_data, token_price_sol, str(sell_tx_hash))
else:
print("Sell transaction failed or no tokens to sell.")
else:
print("Marry mode enabled. Skipping sell operation.")
if not yolo_mode:
break
async def main(yolo_mode=False, match_string=None, bro_address=None, marry_mode=False):
if yolo_mode:
while True:
try:
async with websockets.connect(WSS_ENDPOINT) as websocket:
while True:
try:
await trade(websocket, match_string, bro_address, marry_mode, yolo_mode)
except websockets.exceptions.ConnectionClosed:
print("WebSocket connection closed. Reconnecting...")
break
except Exception as e:
print(f"An error occurred: {e}")
print("Waiting for 5 seconds before looking for the next token...")
await asyncio.sleep(5)
except Exception as e:
print(f"Connection error: {e}")
print("Reconnecting in 5 seconds...")
await asyncio.sleep(5)
else:
# For non-YOLO mode, create a websocket connection and close it after one trade
async with websockets.connect(WSS_ENDPOINT) as websocket:
await trade(websocket, match_string, bro_address, marry_mode, yolo_mode)
async def ping_websocket(websocket):
while True:
try:
await websocket.ping()
await asyncio.sleep(20) # Send a ping every 20 seconds
except:
break
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Trade tokens on Solana.")
parser.add_argument("--yolo", action="store_true", help="Run in YOLO mode (continuous trading)")
parser.add_argument("--match", type=str, help="Only trade tokens with names or symbols matching this string")
parser.add_argument("--bro", type=str, help="Only trade tokens created by this user address")
parser.add_argument("--marry", action="store_true", help="Only buy tokens, skip selling")
args = parser.parse_args()
asyncio.run(main(yolo_mode=args.yolo, match_string=args.match, bro_address=args.bro, marry_mode=args.marry))
@@ -1,9 +0,0 @@
{
"name": "RAKAMAKAFO",
"symbol": "MAKAFO",
"uri": "https://cf-ipfs.com/ipfs/QmYyNfffJL8aLzsQQBxSDEmbQ2STSPS35rfFwE7isjJiVu",
"mint": "LR7TYDeWamU7MsCXFH7ydwJYaZfRJs8pzgCQrh2pump",
"bondingCurve": "9UznPeXvYtPqq8saSyM65cC8LCtyAKPnLCGG7Mq1pUTq",
"associatedBondingCurve": "CTe6pwwgzEUvRmPGRE2EZXrESpiWawK5TwyJhgvFgJ6T",
"user": "Ev5WEEJ2Y1qKGE8KCNvBNVEgy9fJUuttA93pMo2fcGFd"
}
+2 -2
View File
@@ -1,2 +1,2 @@
{"timestamp": "2024-08-22T10:51:30.306580", "action": "buy", "token_address": "LR7TYDeWamU7MsCXFH7ydwJYaZfRJs8pzgCQrh2pump", "price": 1.668947042812354e-07, "tx_hash": "XZDRuEnaBNEeuFAiGyJsqBmVF3uZvFqAQUKsZVKTxsThHxN43LiRqy3T4cSH4dRXGB7c5A4855iYQDLY4BNJJhP"}
{"timestamp": "2024-08-22T10:51:56.116951", "action": "sell", "token_address": "LR7TYDeWamU7MsCXFH7ydwJYaZfRJs8pzgCQrh2pump", "price": 1.668947042812354e-07, "tx_hash": "WQHnEoyztfDf5FJyQRXDGxxtqf2EcMGs8SET43KuYU8Dpdunruku22SHnfiH2xsUi1hTBjC1msvTcnU78fFKrc2"}
{"timestamp": "2025-04-24T20:30:13.087092", "action": "buy", "token_address": "DWMUmRQUZPCBA1gwdDxTJuz6JHnQkREiWMyQpsKWGp9v", "symbol": "U8", "price": 5e-06, "amount": 20, "tx_hash": "3JvdfCep45PUB6rCcH4dB2NuwvFP8n67SCUxqJMt4MuN5ekHYc6J27aCUfwNUK3hh5rSyKNYAWXya5vQAT2qQivB"}
{"timestamp": "2025-04-24T20:30:32.759177", "action": "sell", "token_address": "DWMUmRQUZPCBA1gwdDxTJuz6JHnQkREiWMyQpsKWGp9v", "symbol": "U8", "price": 3.805530050663904e-08, "amount": 20.0, "tx_hash": "5cveLfU7XhPNCPMCZfTXyugJpmAQNmi7zr81PSqs8DsP1T2swYFjJwaB5hNSf3kFPfRzgzd7QZBVaZLd5MqsJevB"}