mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-07-27 15:27:44 +00:00
cc85daaeb3
* chore(pumpswap+docs): post-2026-04-28-cutover PumpSwap unlock + IDL docs Drops the `INCLUDE_BREAKING_FEE_ACCOUNTS = False` gate in the PumpSwap learning examples so the +2 breaking-fee accounts (fee recipient readonly + its quote-mint ATA mutable) are always appended after `pool-v2`. Mainnet pump-amm rejects this format pre-cutover (verified 6023 Overflow), so this PR is intentionally **draft until 2026-04-28 16:00 UTC** when the cutover happens; mark it ready-for-review and live- test then. Also captures the protocol gotchas we learned during this migration: - README: a new "2026-04-28 program upgrade" section + an explicit note that the vendored IDL is incomplete (missing `bonding-curve-v2` and `pool-v2` PDAs that the on-chain program actually requires) with pointers to cross-check against on-chain txs. - CLAUDE.md: a new "Pump.fun protocol notes" subsection summarising the same gotchas plus the BC/Pool/CreateEvent layout details and the extreme_fast_mode gotcha. Open question (call out at review time, resolve post-cutover): - BREAKING_FEE_RECIPIENT.md shows PumpSwap cashback account counts of 27 buy / 26 sell vs 26 / 24 non-cashback — the extra cashback account seed/position isn't documented. Need to sample a real successful cashback PumpSwap tx after cutover and add the branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(pumpswap): wire cashback layout for post-2026-04-28 program upgrade Cashback PumpSwap pools require extra writable accounts inserted before pool-v2. Identified layouts by sampling on-chain post-cutover txs: - buy: insert user_volume_accumulator_quote_ata (27 accounts vs 26) ref tx 4JaWdExj…fvjK - sell: insert user_volume_accumulator_quote_ata + user_volume_accumulator (26 accounts vs 24) ref tx 4ei1cJV7…NP3 Detect via pool account byte 244 (is_cashback_coin). Adds the standalone sample_cashback_pumpswap.py used to reverse-engineer the layouts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
135 lines
4.5 KiB
Python
135 lines
4.5 KiB
Python
"""Post-cutover sampler: find a successful cashback PumpSwap buy/sell on mainnet.
|
|
|
|
Goal: identify the seed/position of the +1 account that the program requires for
|
|
cashback pools (27-account buy / 26-account sell vs 26 / 24 non-cashback).
|
|
|
|
Strategy:
|
|
1. Pull recent signatures for pAMM program.
|
|
2. For each tx, fetch full tx, find the pAMM buy/sell ix.
|
|
3. Resolve the pool account from the ix; fetch its data; check byte 244
|
|
(is_cashback_coin) — only proceed if it's 1.
|
|
4. Print the full account list with counts so we can diff against the known
|
|
26/24 non-cashback layout in manual_buy/sell_pumpswap.py.
|
|
|
|
Usage:
|
|
uv run learning-examples/pumpswap/sample_cashback_pumpswap.py [LIMIT]
|
|
|
|
Env: SOLANA_NODE_RPC_ENDPOINT (defaults to public mainnet)
|
|
"""
|
|
|
|
import asyncio
|
|
import os
|
|
import sys
|
|
|
|
from solana.rpc.async_api import AsyncClient
|
|
from solders.pubkey import Pubkey
|
|
from solders.signature import Signature
|
|
|
|
PUMP_AMM_PROGRAM_ID = Pubkey.from_string("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA")
|
|
BUY_DISCRIMINATOR = bytes.fromhex("66063d1201daebea")
|
|
SELL_DISCRIMINATOR = bytes.fromhex("33e685a4017f83ad")
|
|
|
|
# Pool layout: byte 244 = is_cashback_coin (per CLAUDE.md / PR #167 notes).
|
|
POOL_IS_CASHBACK_OFFSET = 244
|
|
|
|
RPC = os.environ.get(
|
|
"SOLANA_NODE_RPC_ENDPOINT", "https://api.mainnet-beta.solana.com"
|
|
)
|
|
|
|
|
|
async def is_cashback_pool(client: AsyncClient, pool: Pubkey) -> bool | None:
|
|
resp = await client.get_account_info(pool, encoding="base64")
|
|
if resp.value is None:
|
|
return None
|
|
data = resp.value.data
|
|
if len(data) <= POOL_IS_CASHBACK_OFFSET:
|
|
return False
|
|
return data[POOL_IS_CASHBACK_OFFSET] == 1
|
|
|
|
|
|
def classify_ix(ix_data: bytes) -> str | None:
|
|
if ix_data.startswith(BUY_DISCRIMINATOR):
|
|
return "buy"
|
|
if ix_data.startswith(SELL_DISCRIMINATOR):
|
|
return "sell"
|
|
return None
|
|
|
|
|
|
async def inspect_tx(client: AsyncClient, sig: Signature) -> dict | None:
|
|
"""Return diagnostic dict if this tx contains a cashback buy/sell."""
|
|
resp = await client.get_transaction(
|
|
sig, encoding="base64", max_supported_transaction_version=0
|
|
)
|
|
if resp.value is None or resp.value.transaction.meta is None:
|
|
return None
|
|
if resp.value.transaction.meta.err is not None:
|
|
return None # only successful txs
|
|
|
|
tx = resp.value.transaction.transaction
|
|
msg = tx.message
|
|
account_keys = list(msg.account_keys)
|
|
# include loaded addresses from ALTs
|
|
loaded = resp.value.transaction.meta.loaded_addresses
|
|
if loaded is not None:
|
|
account_keys.extend(loaded.writable)
|
|
account_keys.extend(loaded.readonly)
|
|
|
|
for ix in msg.instructions:
|
|
program_id = account_keys[ix.program_id_index]
|
|
if program_id != PUMP_AMM_PROGRAM_ID:
|
|
continue
|
|
kind = classify_ix(bytes(ix.data))
|
|
if kind is None:
|
|
continue
|
|
|
|
# PumpSwap convention: account index 0 of the ix is the pool.
|
|
if not ix.accounts:
|
|
continue
|
|
pool = account_keys[ix.accounts[0]]
|
|
cashback = await is_cashback_pool(client, pool)
|
|
if not cashback:
|
|
continue
|
|
|
|
return {
|
|
"sig": str(sig),
|
|
"kind": kind,
|
|
"pool": str(pool),
|
|
"n_accounts": len(ix.accounts),
|
|
"accounts": [str(account_keys[i]) for i in ix.accounts],
|
|
}
|
|
return None
|
|
|
|
|
|
async def main(limit: int = 200) -> None:
|
|
async with AsyncClient(RPC) as client:
|
|
print(f"Sampling up to {limit} recent pAMM signatures from {RPC}")
|
|
sigs_resp = await client.get_signatures_for_address(
|
|
PUMP_AMM_PROGRAM_ID, limit=limit
|
|
)
|
|
sigs = [s.signature for s in sigs_resp.value if s.err is None]
|
|
print(f" got {len(sigs)} successful signatures")
|
|
|
|
for sig in sigs:
|
|
try:
|
|
hit = await inspect_tx(client, sig)
|
|
except (ValueError, RuntimeError) as e:
|
|
print(f" [skip] {sig}: {e}")
|
|
continue
|
|
if hit is None:
|
|
continue
|
|
print()
|
|
print(f"=== CASHBACK {hit['kind'].upper()} ===")
|
|
print(f" sig: {hit['sig']}")
|
|
print(f" pool: {hit['pool']}")
|
|
print(f" count: {hit['n_accounts']} accounts")
|
|
for i, a in enumerate(hit["accounts"]):
|
|
print(f" [{i:2d}] {a}")
|
|
return
|
|
|
|
print("No cashback PumpSwap buy/sell found in window.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
n = int(sys.argv[1]) if len(sys.argv) > 1 else 200
|
|
asyncio.run(main(n))
|