feat: add example of programSubscribe listener for migrations, group examples into subfolders

This commit is contained in:
smypmsa
2025-04-21 11:36:09 +00:00
parent 188f43f4cd
commit 81c2af4af1
24 changed files with 1148 additions and 255 deletions
@@ -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())