mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-08-19 18:28:06 +00:00
chore(pumpswap): post-2026-04-28 cutover + cashback layout (#168)
* 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>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
22a0c23450
commit
cc85daaeb3
@@ -62,6 +62,15 @@ ruff check
|
|||||||
ruff check --fix
|
ruff check --fix
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Pump.fun protocol notes (gotchas)
|
||||||
|
|
||||||
|
- The vendored IDL at `idl/pump_fun_idl.json` is **incomplete**: it does not list `bonding-curve-v2` (BC trades) or `pool-v2` (PumpSwap trades). Both are required on-chain. `bonding-curve-v2` PDA seed is `["bonding-curve-v2", mint]` under the pump program; `pool-v2` is `["pool-v2", base_mint]` under the pump-amm program. Always cross-check account lists against a recent successful on-chain tx — IDL alone will produce broken code.
|
||||||
|
- BC `buy` ix is **18 accounts** (post 2026-04-28 upgrade). The trailing account is one of 8 `BREAKING_FEE_RECIPIENTS` (mutable), AFTER `bonding-curve-v2`.
|
||||||
|
- BC `sell` ix is **16 accounts non-cashback / 17 cashback**. Cashback path inserts `user_volume_accumulator` (PDA seed `["user_volume_accumulator", user]`) BEFORE `bonding-curve-v2`. Detect cashback from byte 82 of the bonding-curve account data, or via the `is_cashback_coin` field returned by the curve manager.
|
||||||
|
- The BondingCurve account is **83 bytes** (was 81): trailing fields are `is_mayhem_mode: bool` then `is_cashback_coin: bool`. PumpSwap Pool is **245 bytes** with the same `is_cashback_coin` byte at offset 244.
|
||||||
|
- The IDL instruction is `create_v2` (snake_case). `create_v2` args: `name (str), symbol (str), uri (str), creator (pubkey), is_mayhem_mode (bool), is_cashback_enabled (OptionBool 1B)`. `OptionBool` is a struct wrapping a single bool — serialized as 1 byte, not 2.
|
||||||
|
- `extreme_fast_mode` skips the curve-state RPC fetch — make sure event parsers populate `is_mayhem_mode` and `is_cashback_coin` on `TokenInfo` from the `CreateEvent` payload (otherwise mayhem coins use the wrong fee_recipient and cashback coins use the wrong sell account count).
|
||||||
|
|
||||||
## Code Style & Conventions
|
## Code Style & Conventions
|
||||||
|
|
||||||
### Python Style (Ruff Configuration)
|
### Python Style (Ruff Configuration)
|
||||||
|
|||||||
@@ -124,6 +124,22 @@ This helps ensure reliable operation within your node provider's rate limits wit
|
|||||||
|
|
||||||
The IDLs under [`idl/`](idl/) are vendored from [pump-fun/pump-public-docs](https://github.com/pump-fun/pump-public-docs). To refresh, copy `pump.json`, `pump_amm.json`, `pump_fees.json` from that repo into `pump_fun_idl.json`, `pump_swap_idl.json`, `pump_fees.json` respectively, and reference the upstream commit hash in your commit message.
|
The IDLs under [`idl/`](idl/) are vendored from [pump-fun/pump-public-docs](https://github.com/pump-fun/pump-public-docs). To refresh, copy `pump.json`, `pump_amm.json`, `pump_fees.json` from that repo into `pump_fun_idl.json`, `pump_swap_idl.json`, `pump_fees.json` respectively, and reference the upstream commit hash in your commit message.
|
||||||
|
|
||||||
|
> **The IDL is incomplete.** It doesn't list two PDAs that the on-chain program actually requires:
|
||||||
|
> - `bonding-curve-v2` — required on every BC `buy` (18 accounts) and `sell` (16/17 accounts). Seed: `["bonding-curve-v2", mint]` under the pump program.
|
||||||
|
> - `pool-v2` — required on every PumpSwap `buy`/`sell`. Seed: `["pool-v2", base_mint]` under the pump-amm program. **Without it, pump-amm throws `AnchorError 6023 (Overflow)` after the trade transfers complete** — a misleading error code for a missing-account issue.
|
||||||
|
>
|
||||||
|
> Always cross-check your account lists against a recent successful on-chain tx (`getSignaturesForAddress` + `getTransaction`) before trusting the IDL.
|
||||||
|
|
||||||
|
## 2026-04-28 program upgrade
|
||||||
|
|
||||||
|
Pump.fun shipped a breaking program upgrade on **2026-04-28 16:00 UTC** ([BREAKING_FEE_RECIPIENT.md](https://github.com/pump-fun/pump-public-docs/blob/main/docs/BREAKING_FEE_RECIPIENT.md)). The bot is updated for it:
|
||||||
|
|
||||||
|
- BC `buy` ix is now **18 accounts** (was 17). Trailing account is one of 8 `BREAKING_FEE_RECIPIENTS` (mutable), AFTER `bonding-curve-v2`.
|
||||||
|
- BC `sell` ix is now **16 accounts non-cashback / 17 cashback** (was 15/16). Same trailing fee recipient.
|
||||||
|
- PumpSwap `buy`/`sell` get **+2 accounts** appended after `pool-v2`: a fee recipient (readonly) and its quote-mint ATA (mutable). Counts: buy = 26 non-cashback / 27 cashback; sell = 24 / 26. Cashback pools insert `user_volume_accumulator_quote_ata` (writable) BEFORE `pool-v2` on buys; sells insert both that ATA and `user_volume_accumulator` (both writable) BEFORE `pool-v2`. Detect cashback via pool data byte 244.
|
||||||
|
|
||||||
|
The 8 fee recipients are randomized per tx in code (per pump.fun's recommendation to spread program-tx throughput).
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
Quick note on a couple on a few new scripts in `/learning-examples`:
|
Quick note on a couple on a few new scripts in `/learning-examples`:
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ BREAKING_FEE_RECIPIENTS = [
|
|||||||
POOL_DISCRIMINATOR_SIZE = 8
|
POOL_DISCRIMINATOR_SIZE = 8
|
||||||
POOL_BASE_MINT_OFFSET = 43 # Where base_mint field starts in pool account data
|
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_MAYHEM_MODE_OFFSET = 243 # Where is_mayhem_mode flag is stored
|
||||||
|
POOL_IS_CASHBACK_OFFSET = 244 # Where is_cashback_coin flag is stored
|
||||||
POOL_MAYHEM_MODE_MIN_SIZE = 244 # Minimum size for pool data with mayhem flag
|
POOL_MAYHEM_MODE_MIN_SIZE = 244 # Minimum size for pool data with mayhem flag
|
||||||
|
|
||||||
# GlobalConfig structure offsets
|
# GlobalConfig structure offsets
|
||||||
@@ -326,18 +327,11 @@ async def get_reserved_fee_recipient_pumpswap(client: AsyncClient) -> Pubkey:
|
|||||||
|
|
||||||
async def get_pumpswap_fee_recipients(
|
async def get_pumpswap_fee_recipients(
|
||||||
client: AsyncClient, pool: Pubkey
|
client: AsyncClient, pool: Pubkey
|
||||||
) -> tuple[Pubkey, Pubkey]:
|
) -> tuple[Pubkey, Pubkey, bool]:
|
||||||
"""Determine the correct fee recipient based on pool's mayhem mode status.
|
"""Determine the correct fee recipient and whether the pool is cashback.
|
||||||
|
|
||||||
This function checks if mayhem mode is enabled for the pool and returns
|
|
||||||
the appropriate fee recipient and their WSOL token account.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
client: Solana RPC client
|
|
||||||
pool: Address of the AMM pool
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (fee_recipient_pubkey, fee_recipient_token_account)
|
Tuple of (fee_recipient_pubkey, fee_recipient_token_account, is_cashback)
|
||||||
"""
|
"""
|
||||||
response = await client.get_account_info(pool, encoding="base64")
|
response = await client.get_account_info(pool, encoding="base64")
|
||||||
if not response.value or not response.value.data:
|
if not response.value or not response.value.data:
|
||||||
@@ -346,23 +340,23 @@ async def get_pumpswap_fee_recipients(
|
|||||||
|
|
||||||
pool_data = response.value.data
|
pool_data = response.value.data
|
||||||
|
|
||||||
# Check if mayhem mode flag exists and is enabled
|
|
||||||
is_mayhem_mode = len(pool_data) >= POOL_MAYHEM_MODE_MIN_SIZE and bool(
|
is_mayhem_mode = len(pool_data) >= POOL_MAYHEM_MODE_MIN_SIZE and bool(
|
||||||
pool_data[POOL_MAYHEM_MODE_OFFSET]
|
pool_data[POOL_MAYHEM_MODE_OFFSET]
|
||||||
)
|
)
|
||||||
|
is_cashback = len(pool_data) > POOL_IS_CASHBACK_OFFSET and bool(
|
||||||
|
pool_data[POOL_IS_CASHBACK_OFFSET]
|
||||||
|
)
|
||||||
|
|
||||||
# Select appropriate fee recipient
|
|
||||||
if is_mayhem_mode:
|
if is_mayhem_mode:
|
||||||
fee_recipient = await get_reserved_fee_recipient_pumpswap(client)
|
fee_recipient = await get_reserved_fee_recipient_pumpswap(client)
|
||||||
else:
|
else:
|
||||||
fee_recipient = STANDARD_PUMPSWAP_FEE_RECIPIENT
|
fee_recipient = STANDARD_PUMPSWAP_FEE_RECIPIENT
|
||||||
|
|
||||||
# Get the fee recipient's WSOL token account
|
|
||||||
fee_recipient_token_account = get_associated_token_address(
|
fee_recipient_token_account = get_associated_token_address(
|
||||||
fee_recipient, SOL, SYSTEM_TOKEN_PROGRAM
|
fee_recipient, SOL, SYSTEM_TOKEN_PROGRAM
|
||||||
)
|
)
|
||||||
|
|
||||||
return (fee_recipient, fee_recipient_token_account)
|
return (fee_recipient, fee_recipient_token_account, is_cashback)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -483,9 +477,16 @@ async def buy_pump_swap(
|
|||||||
global_volume_accumulator = find_global_volume_accumulator()
|
global_volume_accumulator = find_global_volume_accumulator()
|
||||||
user_volume_accumulator = find_user_volume_accumulator(payer.pubkey())
|
user_volume_accumulator = find_user_volume_accumulator(payer.pubkey())
|
||||||
|
|
||||||
# Get fee recipient based on mayhem mode
|
# Get fee recipient based on mayhem mode + detect cashback pool
|
||||||
fee_recipient, fee_recipient_token_account = await get_pumpswap_fee_recipients(
|
(
|
||||||
client, market
|
fee_recipient,
|
||||||
|
fee_recipient_token_account,
|
||||||
|
is_cashback,
|
||||||
|
) = await get_pumpswap_fee_recipients(client, market)
|
||||||
|
|
||||||
|
# WSOL ATA of user_volume_accumulator — only required for cashback pools.
|
||||||
|
user_volume_accumulator_quote_ata = get_associated_token_address(
|
||||||
|
user_volume_accumulator, SOL, SYSTEM_TOKEN_PROGRAM
|
||||||
)
|
)
|
||||||
|
|
||||||
# Build account list for buy instruction
|
# Build account list for buy instruction
|
||||||
@@ -528,23 +529,34 @@ async def buy_pump_swap(
|
|||||||
AccountMeta(pubkey=user_volume_accumulator, is_signer=False, is_writable=True),
|
AccountMeta(pubkey=user_volume_accumulator, is_signer=False, is_writable=True),
|
||||||
AccountMeta(pubkey=find_fee_config(), is_signer=False, is_writable=False),
|
AccountMeta(pubkey=find_fee_config(), is_signer=False, is_writable=False),
|
||||||
AccountMeta(pubkey=PUMP_FEE_PROGRAM, is_signer=False, is_writable=False),
|
AccountMeta(pubkey=PUMP_FEE_PROGRAM, is_signer=False, is_writable=False),
|
||||||
# pool-v2 PDA (per-base-mint) — the last "pre-upgrade" account.
|
|
||||||
AccountMeta(pubkey=find_pool_v2(base_mint), is_signer=False, is_writable=False),
|
|
||||||
]
|
]
|
||||||
# 2 new accounts required from 2026-04-28 16:00 UTC pump-swap program upgrade,
|
# Cashback pools require user_volume_accumulator_quote_ata (writable) BEFORE
|
||||||
# AFTER pool-v2: breaking-upgrade fee recipient (readonly, second-to-last) and
|
# pool-v2. Confirmed against on-chain post-cutover cashback buy
|
||||||
# its quote-mint ATA (mutable, last). Set to True to opt in.
|
# (sig 4JaWdExj6zzU3aGNWqNFhmtCyhbjRU3zLrsA3vASGu9krQrLjAfbBygP9i7yXmruSbuYn4StgMdMFBi22oQCfvjK).
|
||||||
# Doc: github.com/pump-fun/pump-public-docs/blob/main/docs/BREAKING_FEE_RECIPIENT.md
|
if is_cashback:
|
||||||
INCLUDE_BREAKING_FEE_ACCOUNTS = False # flip True after 2026-04-28 16:00 UTC
|
accounts.append(
|
||||||
if INCLUDE_BREAKING_FEE_ACCOUNTS:
|
AccountMeta(
|
||||||
breaking_fee_recipient = random.choice(BREAKING_FEE_RECIPIENTS)
|
pubkey=user_volume_accumulator_quote_ata,
|
||||||
breaking_fee_quote_ata = get_associated_token_address(
|
is_signer=False,
|
||||||
breaking_fee_recipient, SOL, SYSTEM_TOKEN_PROGRAM
|
is_writable=True,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
accounts.extend([
|
# pool-v2 PDA (per-base-mint) — the last "pre-upgrade" account.
|
||||||
AccountMeta(pubkey=breaking_fee_recipient, is_signer=False, is_writable=False),
|
accounts.append(
|
||||||
AccountMeta(pubkey=breaking_fee_quote_ata, is_signer=False, is_writable=True),
|
AccountMeta(pubkey=find_pool_v2(base_mint), is_signer=False, is_writable=False)
|
||||||
])
|
)
|
||||||
|
# 2 accounts required by the 2026-04-28 pump-swap upgrade, appended AFTER
|
||||||
|
# pool-v2: breaking-fee recipient (readonly) + its quote-mint ATA (mutable).
|
||||||
|
# Buy counts: 26 non-cashback / 27 cashback.
|
||||||
|
# Doc: github.com/pump-fun/pump-public-docs/blob/main/docs/BREAKING_FEE_RECIPIENT.md
|
||||||
|
breaking_fee_recipient = random.choice(BREAKING_FEE_RECIPIENTS)
|
||||||
|
breaking_fee_quote_ata = get_associated_token_address(
|
||||||
|
breaking_fee_recipient, SOL, SYSTEM_TOKEN_PROGRAM
|
||||||
|
)
|
||||||
|
accounts.extend([
|
||||||
|
AccountMeta(pubkey=breaking_fee_recipient, is_signer=False, is_writable=False),
|
||||||
|
AccountMeta(pubkey=breaking_fee_quote_ata, is_signer=False, is_writable=True),
|
||||||
|
])
|
||||||
|
|
||||||
# Instruction data format:
|
# Instruction data format:
|
||||||
# discriminator (8 bytes) + amount_out (8 bytes) + max_in (8 bytes) + track_volume (1 byte)
|
# discriminator (8 bytes) + amount_out (8 bytes) + max_in (8 bytes) + track_volume (1 byte)
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ BREAKING_FEE_RECIPIENTS = [
|
|||||||
POOL_DISCRIMINATOR_SIZE = 8
|
POOL_DISCRIMINATOR_SIZE = 8
|
||||||
POOL_BASE_MINT_OFFSET = 43 # Where base_mint field starts in pool account data
|
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_MAYHEM_MODE_OFFSET = 243 # Where is_mayhem_mode flag is stored
|
||||||
|
POOL_IS_CASHBACK_OFFSET = 244 # Where is_cashback_coin flag is stored
|
||||||
POOL_MAYHEM_MODE_MIN_SIZE = 244 # Minimum size for pool data with mayhem flag
|
POOL_MAYHEM_MODE_MIN_SIZE = 244 # Minimum size for pool data with mayhem flag
|
||||||
|
|
||||||
# GlobalConfig structure offsets
|
# GlobalConfig structure offsets
|
||||||
@@ -246,6 +247,16 @@ def find_pool_v2(base_mint: Pubkey) -> Pubkey:
|
|||||||
return derived_address
|
return derived_address
|
||||||
|
|
||||||
|
|
||||||
|
def find_user_volume_accumulator(user: Pubkey) -> Pubkey:
|
||||||
|
"""Derive the per-user volume accumulator PDA (pump-amm). Required for
|
||||||
|
cashback-pool sells (along with its WSOL ATA)."""
|
||||||
|
derived_address, _ = Pubkey.find_program_address(
|
||||||
|
[b"user_volume_accumulator", bytes(user)],
|
||||||
|
PUMP_AMM_PROGRAM_ID,
|
||||||
|
)
|
||||||
|
return derived_address
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Mayhem Mode Fee Handling
|
# Mayhem Mode Fee Handling
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -279,18 +290,11 @@ async def get_reserved_fee_recipient_pumpswap(client: AsyncClient) -> Pubkey:
|
|||||||
|
|
||||||
async def get_pumpswap_fee_recipients(
|
async def get_pumpswap_fee_recipients(
|
||||||
client: AsyncClient, pool: Pubkey
|
client: AsyncClient, pool: Pubkey
|
||||||
) -> tuple[Pubkey, Pubkey]:
|
) -> tuple[Pubkey, Pubkey, bool]:
|
||||||
"""Determine the correct fee recipient based on pool's mayhem mode status.
|
"""Determine fee recipient + whether the pool is cashback.
|
||||||
|
|
||||||
This function checks if mayhem mode is enabled for the pool and returns
|
|
||||||
the appropriate fee recipient and their WSOL token account.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
client: Solana RPC client
|
|
||||||
pool: Address of the AMM pool
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (fee_recipient_pubkey, fee_recipient_token_account)
|
Tuple of (fee_recipient_pubkey, fee_recipient_token_account, is_cashback)
|
||||||
"""
|
"""
|
||||||
response = await client.get_account_info(pool, encoding="base64")
|
response = await client.get_account_info(pool, encoding="base64")
|
||||||
if not response.value or not response.value.data:
|
if not response.value or not response.value.data:
|
||||||
@@ -299,12 +303,13 @@ async def get_pumpswap_fee_recipients(
|
|||||||
|
|
||||||
pool_data = response.value.data
|
pool_data = response.value.data
|
||||||
|
|
||||||
# Check if mayhem mode flag exists and is enabled
|
|
||||||
is_mayhem_mode = len(pool_data) >= POOL_MAYHEM_MODE_MIN_SIZE and bool(
|
is_mayhem_mode = len(pool_data) >= POOL_MAYHEM_MODE_MIN_SIZE and bool(
|
||||||
pool_data[POOL_MAYHEM_MODE_OFFSET]
|
pool_data[POOL_MAYHEM_MODE_OFFSET]
|
||||||
)
|
)
|
||||||
|
is_cashback = len(pool_data) > POOL_IS_CASHBACK_OFFSET and bool(
|
||||||
|
pool_data[POOL_IS_CASHBACK_OFFSET]
|
||||||
|
)
|
||||||
|
|
||||||
# Select appropriate fee recipient
|
|
||||||
if is_mayhem_mode:
|
if is_mayhem_mode:
|
||||||
fee_recipient = await get_reserved_fee_recipient_pumpswap(client)
|
fee_recipient = await get_reserved_fee_recipient_pumpswap(client)
|
||||||
else:
|
else:
|
||||||
@@ -315,7 +320,7 @@ async def get_pumpswap_fee_recipients(
|
|||||||
fee_recipient, SOL, SYSTEM_TOKEN_PROGRAM
|
fee_recipient, SOL, SYSTEM_TOKEN_PROGRAM
|
||||||
)
|
)
|
||||||
|
|
||||||
return (fee_recipient, fee_recipient_token_account)
|
return (fee_recipient, fee_recipient_token_account, is_cashback)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -475,9 +480,18 @@ async def sell_pump_swap(
|
|||||||
print(f"Selling {token_balance_decimal} tokens")
|
print(f"Selling {token_balance_decimal} tokens")
|
||||||
print(f"Minimum SOL output: {min_sol_output / LAMPORTS_PER_SOL:.10f} SOL")
|
print(f"Minimum SOL output: {min_sol_output / LAMPORTS_PER_SOL:.10f} SOL")
|
||||||
|
|
||||||
# Get fee recipient based on mayhem mode
|
# Get fee recipient based on mayhem mode + detect cashback pool
|
||||||
fee_recipient, fee_recipient_token_account = await get_pumpswap_fee_recipients(
|
(
|
||||||
client, market
|
fee_recipient,
|
||||||
|
fee_recipient_token_account,
|
||||||
|
is_cashback,
|
||||||
|
) = await get_pumpswap_fee_recipients(client, market)
|
||||||
|
|
||||||
|
# Cashback-pool sells append two writable accounts (user_volume_accumulator
|
||||||
|
# quote ATA, then the accumulator PDA itself) BEFORE pool-v2.
|
||||||
|
user_volume_accumulator = find_user_volume_accumulator(payer.pubkey())
|
||||||
|
user_volume_accumulator_quote_ata = get_associated_token_address(
|
||||||
|
user_volume_accumulator, SOL, SYSTEM_TOKEN_PROGRAM
|
||||||
)
|
)
|
||||||
|
|
||||||
# Build account list for sell instruction
|
# Build account list for sell instruction
|
||||||
@@ -516,23 +530,38 @@ async def sell_pump_swap(
|
|||||||
),
|
),
|
||||||
AccountMeta(pubkey=find_fee_config(), is_signer=False, is_writable=False),
|
AccountMeta(pubkey=find_fee_config(), is_signer=False, is_writable=False),
|
||||||
AccountMeta(pubkey=PUMP_FEE_PROGRAM, is_signer=False, is_writable=False),
|
AccountMeta(pubkey=PUMP_FEE_PROGRAM, is_signer=False, is_writable=False),
|
||||||
# pool-v2 PDA (per-base-mint) — the last "pre-upgrade" account.
|
|
||||||
AccountMeta(pubkey=find_pool_v2(base_mint), is_signer=False, is_writable=False),
|
|
||||||
]
|
]
|
||||||
# 2 new accounts required from 2026-04-28 16:00 UTC pump-swap program upgrade,
|
# Cashback pools require user_volume_accumulator_quote_ata + the accumulator
|
||||||
# AFTER pool-v2: breaking-upgrade fee recipient (readonly, second-to-last) and
|
# PDA itself (both writable) BEFORE pool-v2. Confirmed against on-chain
|
||||||
# its quote-mint ATA (mutable, last). Set to True to opt in.
|
# post-cutover cashback sell
|
||||||
# Doc: github.com/pump-fun/pump-public-docs/blob/main/docs/BREAKING_FEE_RECIPIENT.md
|
# (sig 4ei1cJV7uaENJeb5p8prVKiTApTouTh94r9HqPNbj7oJH52X8mEiXhrVNKUgtB9WeZB8jZANnmuSkdTuJ59y8NP3).
|
||||||
INCLUDE_BREAKING_FEE_ACCOUNTS = False # flip True after 2026-04-28 16:00 UTC
|
if is_cashback:
|
||||||
if INCLUDE_BREAKING_FEE_ACCOUNTS:
|
|
||||||
breaking_fee_recipient = random.choice(BREAKING_FEE_RECIPIENTS)
|
|
||||||
breaking_fee_quote_ata = get_associated_token_address(
|
|
||||||
breaking_fee_recipient, SOL, SYSTEM_TOKEN_PROGRAM
|
|
||||||
)
|
|
||||||
accounts.extend([
|
accounts.extend([
|
||||||
AccountMeta(pubkey=breaking_fee_recipient, is_signer=False, is_writable=False),
|
AccountMeta(
|
||||||
AccountMeta(pubkey=breaking_fee_quote_ata, is_signer=False, is_writable=True),
|
pubkey=user_volume_accumulator_quote_ata,
|
||||||
|
is_signer=False,
|
||||||
|
is_writable=True,
|
||||||
|
),
|
||||||
|
AccountMeta(
|
||||||
|
pubkey=user_volume_accumulator, is_signer=False, is_writable=True
|
||||||
|
),
|
||||||
])
|
])
|
||||||
|
# pool-v2 PDA (per-base-mint) — the last "pre-upgrade" account.
|
||||||
|
accounts.append(
|
||||||
|
AccountMeta(pubkey=find_pool_v2(base_mint), is_signer=False, is_writable=False)
|
||||||
|
)
|
||||||
|
# 2 accounts required by the 2026-04-28 pump-swap upgrade, appended AFTER
|
||||||
|
# pool-v2: breaking-fee recipient (readonly) + its quote-mint ATA (mutable).
|
||||||
|
# Sell counts: 24 non-cashback / 26 cashback.
|
||||||
|
# Doc: github.com/pump-fun/pump-public-docs/blob/main/docs/BREAKING_FEE_RECIPIENT.md
|
||||||
|
breaking_fee_recipient = random.choice(BREAKING_FEE_RECIPIENTS)
|
||||||
|
breaking_fee_quote_ata = get_associated_token_address(
|
||||||
|
breaking_fee_recipient, SOL, SYSTEM_TOKEN_PROGRAM
|
||||||
|
)
|
||||||
|
accounts.extend([
|
||||||
|
AccountMeta(pubkey=breaking_fee_recipient, is_signer=False, is_writable=False),
|
||||||
|
AccountMeta(pubkey=breaking_fee_quote_ata, is_signer=False, is_writable=True),
|
||||||
|
])
|
||||||
|
|
||||||
# Instruction data format: discriminator (8 bytes) + amount (8 bytes) + min_out (8 bytes)
|
# Instruction data format: discriminator (8 bytes) + amount (8 bytes) + min_out (8 bytes)
|
||||||
# All integers are little-endian (<)
|
# All integers are little-endian (<)
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
"""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))
|
||||||
Reference in New Issue
Block a user