mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-08-20 18:58:05 +00:00
feat(pumpfun): migrate to buy_v2/sell_v2 and support non-SOL quote assets (#176)
Refresh the vendored IDLs from pump-fun/pump-public-docs @ 9c82f61 and move all pump.fun trading onto the v2 instruction interface. This is required, not optional: legacy buy/sell cannot trade coins paired against a quote asset other than SOL, and USDC is already whitelisted in the on-chain Global account. Protocol changes absorbed: - buy_v2 (27 accounts) / sell_v2 (26 accounts) replace the legacy instructions. Every account is mandatory and the order is identical for all coins, so the conditional cashback/mayhem account lists are gone. Legacy remains available via PumpFunInstructionBuilder(use_legacy_instructions=True). - BondingCurve is 151 bytes: virtual_sol_reserves -> virtual_quote_reserves, real_sol_reserves -> real_quote_reserves, plus quote_mint at offset 83. Old field names are kept as aliases so existing callers keep working. - v2 instruction data drops the track_volume OptionBool; amounts are in the quote mint's raw units rather than always lamports. - create_v2 carries a non-SOL quote mint as optional remaining accounts 17-19, and CreateEvent gained quote_mint, so extreme_fast_mode can resolve the quote asset without an extra fetch. USDC support: new trade.quote_amounts and filters.allowed_quote_mints config, accepting "sol"/"usdc" aliases or raw mints. Amounts are per-quote-mint because 1 USDC and 1 SOL are not interchangeable. A coin whose quote mint has no configured amount is skipped rather than traded at the wrong size, so SOL-only configs are unaffected. Bug fixes found while verifying: - The logs and blocks listeners set no websocket max_size, so any frame over 1 MiB closed the connection with 1009 and the token in it was lost. Raised to 32 MiB. - PumpSwap priced against the raw quote vault balance, ignoring the new Pool.virtual_quote_reserves (i128 at offset 245; live pools are 301 bytes). Upstream's note that this field is 0 everywhere is out of date: a live pool carries 17.58 SOL against a 148 SOL vault, a 10.15% price error. - The seller read curve state once at confirmed commitment and silently fell back to create-time values, risking a stale creator_vault and ConstraintSeeds. It now retries at processed, matching the buyer. - Account cleanup would burn wrapped SOL when force_burn was set, destroying value that closing the account returns. WSOL is now closed without burning. - The mint scripts treated a landed transaction as a successful one, so a reverted buy printed as success. They now assert the on-chain result. Compute unit limits retuned from mainnet measurements: buy 100k -> 180k, sell 60k -> 120k. Mint-and-buy is no longer atomic, because create_v2 plus buy_v2 exceeds the 1232-byte transaction limit; both mint scripts send two transactions. Adds learning-examples/pump_v2.py as one shared, standalone v2 toolkit for the example scripts, and three verification scripts: an offline layout check against the IDL, a no-funds mainnet simulation, and a live listener matrix that buys, sells and closes the ATA per listener. Verified on mainnet: all four listeners (geyser, logs, blocks, pumpportal) and all eight example scripts completed a real buy, sell and ATA close, each confirmed by reading the transaction result back rather than trusting confirmation alone. The USDC path is verified structurally only; no USDC-paired coin could be found on-chain to exercise it. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3b88a06d9d
commit
02343b775b
@@ -92,7 +92,11 @@ BREAKING_FEE_RECIPIENTS = [
|
||||
POOL_DISCRIMINATOR_SIZE = 8
|
||||
POOL_BASE_MINT_OFFSET = 43 # Where base_mint field starts in pool account data
|
||||
POOL_MAYHEM_MODE_OFFSET = 243 # Where is_mayhem_mode flag is stored
|
||||
POOL_IS_CASHBACK_OFFSET = 244 # Where is_cashback_coin flag is stored
|
||||
POOL_IS_CASHBACK_OFFSET = 244
|
||||
# virtual_quote_reserves is an i128 appended after the flags. Pool fields
|
||||
# end at 261; live accounts are 301 bytes with trailing padding.
|
||||
POOL_VIRTUAL_QUOTE_RESERVES_OFFSET = 245
|
||||
POOL_VIRTUAL_QUOTE_RESERVES_SIZE = 16
|
||||
POOL_MAYHEM_MODE_MIN_SIZE = 244 # Minimum size for pool data with mayhem flag
|
||||
|
||||
# GlobalConfig structure offsets
|
||||
@@ -176,6 +180,11 @@ async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict:
|
||||
("pool_quote_token_account", "pubkey"),
|
||||
("lp_supply", "u64"),
|
||||
("coin_creator", "pubkey"),
|
||||
# Appended after coin_creator: is_mayhem_mode (243), is_cashback_coin
|
||||
# (244), then virtual_quote_reserves as an i128 at 245..261. Live pool
|
||||
# accounts are 301 bytes (fields end at 261, rest is padding).
|
||||
("is_mayhem_mode", "u8"),
|
||||
("is_cashback_coin", "u8"),
|
||||
]
|
||||
|
||||
for field_name, field_type in fields:
|
||||
@@ -328,23 +337,54 @@ async def get_pumpswap_fee_recipients(
|
||||
# ============================================================================
|
||||
|
||||
|
||||
async def read_virtual_quote_reserves(client: AsyncClient, pool: Pubkey) -> int:
|
||||
"""Read Pool::virtual_quote_reserves, the field appended after the flags.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client
|
||||
pool: Pool (market) address
|
||||
|
||||
Returns:
|
||||
Raw virtual quote reserves, or 0 if the account predates the field
|
||||
"""
|
||||
response = await client.get_account_info(pool, encoding="base64")
|
||||
if not response.value or not response.value.data:
|
||||
return 0
|
||||
data = response.value.data
|
||||
end = POOL_VIRTUAL_QUOTE_RESERVES_OFFSET + POOL_VIRTUAL_QUOTE_RESERVES_SIZE
|
||||
if len(data) < end:
|
||||
return 0
|
||||
return int.from_bytes(
|
||||
data[POOL_VIRTUAL_QUOTE_RESERVES_OFFSET : end], "little", signed=True
|
||||
)
|
||||
|
||||
|
||||
async def calculate_token_pool_price(
|
||||
client: AsyncClient,
|
||||
pool_base_token_account: Pubkey,
|
||||
pool_quote_token_account: Pubkey,
|
||||
virtual_quote_reserves: int = 0,
|
||||
) -> float:
|
||||
"""Calculate current token price from AMM pool balances.
|
||||
"""Calculate current token price from AMM pool reserves.
|
||||
|
||||
AMM price is determined by the ratio of tokens in the pool:
|
||||
price = quote_balance / base_balance
|
||||
Price is the ratio of *effective* quote reserves to base reserves:
|
||||
|
||||
effective_quote_reserves =
|
||||
pool_quote_token_account.amount + Pool::virtual_quote_reserves
|
||||
|
||||
PumpSwap added `virtual_quote_reserves` to the Pool account. Upstream's
|
||||
release note says it is 0 on every pool, but that is out of date: live pools
|
||||
carry non-zero values (17.58 SOL observed on a 148 SOL pool, i.e. quoting
|
||||
off the raw vault balance under-prices by ~10.6%). Always add it.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client
|
||||
pool_base_token_account: Pool's token account (the token being priced)
|
||||
pool_quote_token_account: Pool's SOL account (the quote currency)
|
||||
pool_quote_token_account: Pool's quote account
|
||||
virtual_quote_reserves: Pool::virtual_quote_reserves, in raw quote units
|
||||
|
||||
Returns:
|
||||
Price in SOL per token
|
||||
Price in quote asset per token
|
||||
"""
|
||||
base_balance_resp = await client.get_token_account_balance(pool_base_token_account)
|
||||
quote_balance_resp = await client.get_token_account_balance(
|
||||
@@ -352,7 +392,9 @@ async def calculate_token_pool_price(
|
||||
)
|
||||
|
||||
base_amount = float(base_balance_resp.value.ui_amount)
|
||||
quote_amount = float(quote_balance_resp.value.ui_amount)
|
||||
quote_decimals = int(quote_balance_resp.value.decimals)
|
||||
quote_raw = int(quote_balance_resp.value.amount) + int(virtual_quote_reserves)
|
||||
quote_amount = quote_raw / 10**quote_decimals
|
||||
|
||||
return quote_amount / base_amount
|
||||
|
||||
@@ -469,7 +511,10 @@ async def sell_pump_swap(
|
||||
return None
|
||||
|
||||
token_price_sol = await calculate_token_pool_price(
|
||||
client, pool_base_token_account, pool_quote_token_account
|
||||
client,
|
||||
pool_base_token_account,
|
||||
pool_quote_token_account,
|
||||
await read_virtual_quote_reserves(client, market),
|
||||
)
|
||||
print(f"Price per Token: {token_price_sol:.20f} SOL")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user