mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-07-27 23:37:45 +00:00
Merge pull request #94 from chainstacklabs/refactored/main-v2
Promoting refactored bot to the main branch
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
SOLANA_NODE_RPC_ENDPOINT=...
|
||||
SOLANA_NODE_WSS_ENDPOINT=...
|
||||
GEYSER_ENDPOINT=...
|
||||
GEYSER_API_TOKEN=...
|
||||
SOLANA_PRIVATE_KEY=...
|
||||
+8
-1
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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())
|
||||
@@ -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())
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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]}")
|
||||
|
||||
@@ -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())
|
||||
+29
-14
@@ -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())
|
||||
@@ -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
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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.")
|
||||
@@ -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()
|
||||
@@ -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}")
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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
@@ -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 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
@@ -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,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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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}")
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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))
|
||||
@@ -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
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user