mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-08-13 07:18:04 +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
@@ -1,10 +1,10 @@
|
||||
import asyncio
|
||||
import os
|
||||
import random
|
||||
import struct
|
||||
from typing import Final
|
||||
|
||||
import base58
|
||||
import pump_v2
|
||||
from dotenv import load_dotenv
|
||||
from solana.rpc.async_api import AsyncClient
|
||||
from solana.rpc.commitment import Confirmed
|
||||
@@ -166,6 +166,32 @@ def _find_bonding_curve_v2(mint: Pubkey) -> Pubkey:
|
||||
return derived_address
|
||||
|
||||
|
||||
async def assert_transaction_succeeded(client: AsyncClient, signature) -> None:
|
||||
"""Raise if a confirmed transaction actually failed on-chain.
|
||||
|
||||
`confirm_transaction` only waits for the transaction to land — a landed
|
||||
transaction can still have reverted. Without this check a failed buy prints
|
||||
as a success, which is exactly how a wrong fee recipient (NotAuthorized,
|
||||
6000) can look like a passing test.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client
|
||||
signature: Transaction signature to inspect
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the transaction reverted
|
||||
"""
|
||||
result = await client.get_transaction(
|
||||
signature, commitment="confirmed", max_supported_transaction_version=0
|
||||
)
|
||||
value = result.value
|
||||
if value is None:
|
||||
raise RuntimeError(f"Transaction {signature} not found after confirmation")
|
||||
err = value.transaction.meta.err if value.transaction.meta else None
|
||||
if err:
|
||||
raise RuntimeError(f"Transaction {signature} landed but failed on-chain: {err}")
|
||||
|
||||
|
||||
def create_pump_create_instruction(
|
||||
mint: Pubkey,
|
||||
mint_authority: Pubkey,
|
||||
@@ -251,68 +277,46 @@ def create_buy_instruction(
|
||||
token_amount: int,
|
||||
max_sol_cost: int,
|
||||
track_volume: bool = True,
|
||||
is_mayhem_mode: bool = False,
|
||||
) -> Instruction:
|
||||
"""Create the buy instruction."""
|
||||
accounts = [
|
||||
AccountMeta(pubkey=global_state, is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=fee_recipient, 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_user, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=user, 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=creator_vault, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False),
|
||||
AccountMeta(
|
||||
pubkey=_find_global_volume_accumulator(), is_signer=False, is_writable=False
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=_find_user_volume_accumulator(user),
|
||||
is_signer=False,
|
||||
is_writable=True,
|
||||
),
|
||||
# Index 14: fee_config (readonly)
|
||||
AccountMeta(
|
||||
pubkey=_find_fee_config(),
|
||||
is_signer=False,
|
||||
is_writable=False,
|
||||
),
|
||||
# Index 15: fee_program (readonly)
|
||||
AccountMeta(
|
||||
pubkey=PUMP_FEE_PROGRAM,
|
||||
is_signer=False,
|
||||
is_writable=False,
|
||||
),
|
||||
# Remaining account: bonding_curve_v2 (readonly, required for all coins)
|
||||
AccountMeta(
|
||||
pubkey=_find_bonding_curve_v2(mint),
|
||||
is_signer=False,
|
||||
is_writable=False,
|
||||
),
|
||||
# 18th account: breaking-upgrade fee recipient (mutable) — required from 2026-04-28
|
||||
AccountMeta(
|
||||
pubkey=random.choice(BREAKING_FEE_RECIPIENTS),
|
||||
is_signer=False,
|
||||
is_writable=True,
|
||||
),
|
||||
]
|
||||
"""Create the buy instruction (buy_v2).
|
||||
|
||||
# Encode OptionBool for track_volume
|
||||
# OptionBool: [0] = None, [1, 0] = Some(false), [1, 1] = Some(true)
|
||||
track_volume_bytes = bytes([1, 1 if track_volume else 0])
|
||||
The signature is unchanged for callers, but this builds `buy_v2` with its 27
|
||||
mandatory accounts. Several parameters are accepted only for backwards
|
||||
compatibility and are derived or dropped internally: buy_v2 takes no
|
||||
track_volume argument, and pump_v2 selects the fee recipient from the
|
||||
documented set. This script mints the coin with `creator = payer`, so the
|
||||
buyer is also the creator.
|
||||
|
||||
data = (
|
||||
BUY_DISCRIMINATOR
|
||||
+ struct.pack("<Q", token_amount)
|
||||
+ struct.pack("<Q", max_sol_cost)
|
||||
+ track_volume_bytes
|
||||
Args:
|
||||
global_state: Unused; pump_v2 uses the canonical global PDA
|
||||
fee_recipient: Unused; pump_v2 selects from the documented set
|
||||
mint: Base token mint just created
|
||||
bonding_curve: Unused; derived from the mint
|
||||
associated_bonding_curve: Unused; derived
|
||||
associated_user: Unused; derived
|
||||
user: Buyer, and the coin's creator in this script
|
||||
creator_vault: Unused; derived from the creator
|
||||
token_amount: Base tokens to buy, raw units
|
||||
max_sol_cost: Spend cap in lamports
|
||||
track_volume: Ignored; volume tracking is unconditional under buy_v2
|
||||
is_mayhem_mode: Mayhem coins must use a *reserved* fee recipient; passing
|
||||
this wrong makes the program reject the buy with NotAuthorized (6000)
|
||||
|
||||
Returns:
|
||||
The buy_v2 instruction
|
||||
"""
|
||||
return pump_v2.build_buy_v2_instruction(
|
||||
base_mint=mint,
|
||||
creator=user,
|
||||
user=user,
|
||||
token_amount_raw=token_amount,
|
||||
max_quote_cost_raw=max_sol_cost,
|
||||
quote_mint=pump_v2.WSOL_MINT,
|
||||
is_mayhem_mode=is_mayhem_mode,
|
||||
base_token_program=SYSTEM_TOKEN_PROGRAM,
|
||||
)
|
||||
|
||||
return Instruction(PUMP_PROGRAM, data, accounts)
|
||||
|
||||
|
||||
async def main():
|
||||
"""Create and buy pump.fun token in a single transaction."""
|
||||
@@ -385,14 +389,19 @@ async def main():
|
||||
bonding_curve=bonding_curve,
|
||||
user=payer.pubkey(),
|
||||
),
|
||||
# Create user ATA
|
||||
]
|
||||
|
||||
# buy_v2's 27 accounts push a combined create+buy message past Solana's
|
||||
# 1232-byte packet limit (measured 1972 bytes for the v2 pair), so the buy
|
||||
# goes in a second transaction. The legacy 18-account buy used to fit;
|
||||
# recovering atomicity would need an address lookup table.
|
||||
buy_instructions = [
|
||||
create_idempotent_associated_token_account(
|
||||
payer.pubkey(),
|
||||
payer.pubkey(),
|
||||
mint_keypair.pubkey(),
|
||||
SYSTEM_TOKEN_PROGRAM,
|
||||
),
|
||||
# Buy tokens
|
||||
create_buy_instruction(
|
||||
global_state=PUMP_GLOBAL,
|
||||
fee_recipient=PUMP_FEE,
|
||||
@@ -416,18 +425,33 @@ async def main():
|
||||
[payer, mint_keypair], message, recent_blockhash.value.blockhash
|
||||
)
|
||||
|
||||
print("\nSending transaction...")
|
||||
print("\nSending create transaction...")
|
||||
opts = TxOpts(skip_preflight=True, preflight_commitment=Confirmed)
|
||||
|
||||
try:
|
||||
response = await client.send_transaction(transaction, opts)
|
||||
tx_hash = response.value
|
||||
|
||||
print(f"Transaction sent: https://solscan.io/tx/{tx_hash}")
|
||||
print(f"Create sent: https://solscan.io/tx/{tx_hash}")
|
||||
|
||||
print("Waiting for confirmation...")
|
||||
await client.confirm_transaction(tx_hash, commitment="confirmed")
|
||||
print("Transaction confirmed!")
|
||||
await assert_transaction_succeeded(client, tx_hash)
|
||||
print("Create confirmed!")
|
||||
|
||||
buy_blockhash = await client.get_latest_blockhash()
|
||||
buy_tx = Transaction(
|
||||
[payer],
|
||||
Message(buy_instructions, payer.pubkey()),
|
||||
buy_blockhash.value.blockhash,
|
||||
)
|
||||
print("\nSending buy transaction (buy_v2)...")
|
||||
buy_response = await client.send_transaction(buy_tx, opts)
|
||||
buy_hash = buy_response.value
|
||||
print(f"Buy sent: https://solscan.io/tx/{buy_hash}")
|
||||
await client.confirm_transaction(buy_hash, commitment="confirmed")
|
||||
await assert_transaction_succeeded(client, buy_hash)
|
||||
print("Buy confirmed!")
|
||||
|
||||
return tx_hash
|
||||
|
||||
|
||||
Reference in New Issue
Block a user