Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ lists; these are the ones this CLI depends on.
pool, then `pool-v2` **only on a canonical pool** (`coin_creator` set), then the
buyback recipient and its quote ATA last. The canonical pool is derived from the
mint and the curve's quote mint.
- `migrate_v2` also carries accounts the IDL omits: after its 27, the new pool's
boost vault authority (`["boost_vault", pool]` on PumpSwap) and that
authority's quote token account. Without them PumpSwap reverts with 6027.
- `send_tx` sends v0 and falls back to v1 when the transaction exceeds 1232
bytes (a cashback-pool buy, a launch with a buy). v1 carries the compute budget
in its header.
Expand Down
6 changes: 6 additions & 0 deletions src/pumpfun_cli/protocol/address.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,12 @@ def derive_amm_pool(mint: Pubkey, quote_mint: Pubkey = WSOL_MINT) -> Pubkey:
return addr


def derive_amm_boost_vault_authority(pool: Pubkey) -> Pubkey:
"""Derive a PumpSwap pool's boost vault authority PDA."""
addr, _ = Pubkey.find_program_address([b"boost_vault", bytes(pool)], PUMP_AMM_PROGRAM)
return addr


def derive_amm_lp_mint(pool: Pubkey) -> Pubkey:
"""Derive a PumpSwap pool's LP mint."""
addr, _ = Pubkey.find_program_address([b"pool_lp_mint", bytes(pool)], PUMP_AMM_PROGRAM)
Expand Down
11 changes: 10 additions & 1 deletion src/pumpfun_cli/protocol/instructions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from spl.token.models import CloseAccountParams, InitializeAccountParams

from pumpfun_cli.protocol.address import (
derive_amm_boost_vault_authority,
derive_amm_creator_vault,
derive_amm_fee_config,
derive_amm_global_volume_accumulator,
Expand Down Expand Up @@ -654,12 +655,15 @@ def build_migrate_v2_instruction(
) -> Instruction:
"""Move a complete curve's liquidity into its canonical PumpSwap pool.

`withdraw_authority` is read from pump.fun's Global account.
`withdraw_authority` is read from pump.fun's Global account. After the IDL's
27 accounts come the new pool's boost vault authority and its quote token
account: PumpSwap creates the boost vault during the migration.
"""
bonding_curve = derive_bonding_curve(mint)
pool_authority = derive_pool_authority(mint)
pool = derive_amm_pool(mint, quote_mint)
lp_mint = derive_amm_lp_mint(pool)
boost_vault_authority = derive_amm_boost_vault_authority(pool)
return Instruction(
PUMP_PROGRAM,
MIGRATE_V2_DISCRIMINATOR,
Expand Down Expand Up @@ -691,5 +695,10 @@ def build_migrate_v2_instruction(
_meta(RENT),
_meta(PUMP_EVENT_AUTHORITY),
_meta(PUMP_PROGRAM),
_meta(boost_vault_authority),
_meta(
find_ata(boost_vault_authority, quote_mint, quote_token_program),
writable=True,
),
],
)
56 changes: 56 additions & 0 deletions tests/test_protocol/test_extras_instructions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from solders.pubkey import Pubkey

from pumpfun_cli.protocol.address import (
derive_amm_boost_vault_authority,
derive_amm_creator_vault,
derive_amm_lp_mint,
derive_amm_pool,
Expand Down Expand Up @@ -80,3 +81,58 @@ def test_migrate_v2_targets_the_quote_specific_pool():
assert keys[15] == derive_amm_lp_mint(pool)
assert keys[18] == find_ata(pool, QUOTE, TOKEN_2022_PROGRAM)
assert keys[24] == RENT


def test_migrate_v2_appends_the_pools_boost_vault():
keys = _keys(
build_migrate_v2_instruction(
mint=MINT,
user=USER,
withdraw_authority=Pubkey.new_unique(),
quote_mint=QUOTE,
quote_token_program=TOKEN_2022_PROGRAM,
)
)
authority = derive_amm_boost_vault_authority(derive_amm_pool(MINT, QUOTE))
assert keys[27:] == [authority, find_ata(authority, QUOTE, TOKEN_2022_PROGRAM)]


# A migrate_v2 that landed on mainnet (SOL-priced coin), account by account.
MAINNET_MIGRATE_V2 = [
"4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf",
"39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg",
"AvgXW4hP7ozPe8P2VVnyEcCgfyKGotsKsKTzVhHSpump",
"So11111111111111111111111111111111111111112",
"4SxVXjWAVRJtiQz3c15jEvyMVchLnYypphtGwPo4Ue1p",
"Gy7y5cqYHWujSgAkhqauMVuup1etnYBaZcd7EJwpTRSD",
"27gKsZFVnG1dsrUEiqo7JeXNeihmWbLFhwKk5fxaK16Q",
"9C4nRvhhVquCKATjDCx5FKvNS9PNgNqgyWy9AcoDjYv5",
"11111111111111111111111111111111",
"pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA",
"4nUaWTVN4tXficDXbDPBa1zkz22mMaKsSc9PcZVxkDLU",
"Cq2HTwfL5atMtti6X5kGxu2tJcfHBfTBgPibEg3kSp1X",
"EYjAvmPPvryzoEcUHg7tW3spfwvp8WyGhVdgNVAoh36i",
"EscT6HjfPeMG7zrgPSrxP6EEdZPTVL61RrbdRQdTgS4x",
"ADyA8hdefvWN2dbGGWFotbzWxrAvLW83WG6QCVXvJKqw",
"BHLrVmhAMT8SwM3pmwxboXaPmucPmG9F4SECdBEqsJRi",
"CdEVwiDne4DgfYtZv8YzF5g8rnXsrRp8QXHh3RED5M48",
"67vgDuzFqPZaCvRjgjBv1sHrPpn7i9WaVEinjeLXBKUr",
"9Q3LxsWLB3rMzD3FibrMkn1KMrqRwe9dvM4vv9C9EvwB",
"TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
"GS4CU59F31iL7aR2Q8zVS8DRrcRnXX1yjQ66TqNVQnaR",
"SysvarRent111111111111111111111111111111111",
"Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1",
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P",
"4jAkipqL5ESZJBau53Lwa8XMhtuRuvMPtSDXyEroxwC7",
"3edD3kcShSfFwXYgeiWXhxNikatkKbbK2ww5thELSW8y",
]


def test_migrate_v2_matches_a_mainnet_migration():
want = [Pubkey.from_string(k) for k in MAINNET_MIGRATE_V2]
built = build_migrate_v2_instruction(mint=want[2], user=want[7], withdraw_authority=want[1])
assert _keys(built) == want
assert built.accounts[-1].is_writable and not built.accounts[-2].is_writable
2 changes: 1 addition & 1 deletion tests/test_protocol/test_instructions.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def assert_matches_idl(idl: dict, name: str, built: Instruction, prefix: int | N
(
"migrate_v2",
lambda: ix.build_migrate_v2_instruction(mint=MINT, user=USER, withdraw_authority=CREATOR),
27,
29,
),
]

Expand Down
87 changes: 87 additions & 0 deletions tests/test_surfpool/test_extras.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,90 @@ async def test_collect_creator_fees_no_fees(
)
assert result["error"] == "no_fees"
assert "No creator fees" in result["message"]


@pytest.mark.asyncio
async def test_migrate_a_completed_curve(
surfpool_rpc, funded_keypair, test_keystore, test_password
):
"""Launch a coin, buy its whole curve, then migrate it into its PumpSwap pool."""
from solana.rpc.async_api import AsyncClient
from solders.pubkey import Pubkey

from pumpfun_cli.core.info import get_token_info
from pumpfun_cli.core.launch import launch_token
from pumpfun_cli.core.trade import buy_token
from pumpfun_cli.protocol.address import derive_amm_pool, derive_bonding_curve
from pumpfun_cli.protocol.client import RpcClient
from pumpfun_cli.protocol.curve import parse_bonding_curve
from pumpfun_cli.protocol.instructions import build_buy_v2_instruction

client = AsyncClient(surfpool_rpc)
try:
await client.request_airdrop(funded_keypair.pubkey(), 200_000_000_000)
finally:
await client.close()

launched = await launch_token(
rpc_url=surfpool_rpc,
keystore_path=str(test_keystore),
password=test_password,
name="Migrate",
ticker="MIG",
description="migrate test",
uri="https://example.com/migrate.json",
confirm=True,
)
assert "error" not in launched, launched
mint = launched["mint"]
mint_pk = Pubkey.from_string(mint)

# A small buy creates the token account; buy_v2 then takes every token left.
bought = await buy_token(
rpc_url=surfpool_rpc,
keystore_path=str(test_keystore),
password=test_password,
mint_str=mint,
amount=0.01,
confirm=True,
)
assert "error" not in bought, bought
client = RpcClient(surfpool_rpc)
try:
curve = parse_bonding_curve(
bytes((await client.get_account_info(derive_bonding_curve(mint_pk))).value.data)
)
await client.send_tx(
[
build_buy_v2_instruction(
mint=mint_pk,
creator=curve["creator"],
user=funded_keypair.pubkey(),
token_amount=curve["real_token_reserves"],
max_quote_cost=150_000_000_000,
)
],
[funded_keypair],
compute_units=400_000,
confirm=True,
)
finally:
await client.close()

migrated = await migrate_token(
rpc_url=surfpool_rpc,
keystore_path=str(test_keystore),
password=test_password,
mint_str=mint,
confirm=True,
)
assert "error" not in migrated, migrated

client = AsyncClient(surfpool_rpc)
try:
pool = await client.get_account_info(derive_amm_pool(mint_pk))
finally:
await client.close()
assert pool.value is not None
info = await get_token_info(surfpool_rpc, mint)
assert info["graduated"] is True
Loading