From ad25194e82d122c7c905b801b04c8fff98c151c9 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Wed, 18 Mar 2026 16:47:54 +0000 Subject: [PATCH 1/2] feat: add --cashback flag to token launch + fix create_v2 account layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire is_cashback_enabled (OptionBool) through all three layers for the create_v2 instruction, and fix a pre-existing bug where mayhem accounts were conditionally included — the IDL requires them always. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/pumpfun_cli/commands/launch.py | 4 + src/pumpfun_cli/core/launch.py | 3 + src/pumpfun_cli/protocol/instructions.py | 25 ++- tests/test_commands/test_smoke.py | 8 + tests/test_core/test_launch.py | 91 +++++++++ tests/test_protocol/test_instructions.py | 68 +++++++ tests/test_surfpool/test_launch.py | 231 +++++++++++++++++++++++ 7 files changed, 417 insertions(+), 13 deletions(-) create mode 100644 tests/test_core/test_launch.py create mode 100644 tests/test_surfpool/test_launch.py diff --git a/src/pumpfun_cli/commands/launch.py b/src/pumpfun_cli/commands/launch.py index 5e32549..4379845 100644 --- a/src/pumpfun_cli/commands/launch.py +++ b/src/pumpfun_cli/commands/launch.py @@ -16,6 +16,7 @@ def launch( image: str | None = typer.Option(None, "--image", help="Path to token image"), buy: float | None = typer.Option(None, "--buy", help="Initial buy amount in SOL"), mayhem: bool = typer.Option(False, "--mayhem", help="Enable mayhem mode"), + cashback: bool = typer.Option(False, "--cashback", help="Enable cashback for the token"), ): """Launch a new token on pump.fun (create_v2 + extend_account).""" state = ctx.obj @@ -49,6 +50,7 @@ def launch( image, buy, mayhem, + cashback, **overrides, ) ) @@ -63,5 +65,7 @@ def launch( typer.echo(f" Mint: {result['mint']}") typer.echo(f" TX: {result['explorer']}") typer.echo(f" Pump.fun: {result['pump_url']}") + if result.get("is_cashback"): + typer.echo(" Cashback: enabled") if result.get("initial_buy_sol"): typer.echo(f" Buy: {result['initial_buy_sol']} SOL") diff --git a/src/pumpfun_cli/core/launch.py b/src/pumpfun_cli/core/launch.py index 84ec9bc..62bae83 100644 --- a/src/pumpfun_cli/core/launch.py +++ b/src/pumpfun_cli/core/launch.py @@ -58,6 +58,7 @@ async def launch_token( image_path: str | None = None, initial_buy_sol: float | None = None, is_mayhem: bool = False, + is_cashback: bool = False, priority_fee: int | None = None, compute_units: int | None = None, ) -> dict: @@ -84,6 +85,7 @@ async def launch_token( symbol=ticker, uri=uri, is_mayhem=is_mayhem, + is_cashback=is_cashback, ) # 4. Add extend_account instruction (required for frontend visibility) @@ -121,6 +123,7 @@ async def launch_token( "ticker": ticker, "mint": str(mint), "metadata_uri": uri, + "is_cashback": is_cashback, "initial_buy_sol": initial_buy_sol, "signature": sig, "explorer": f"https://solscan.io/tx/{sig}", diff --git a/src/pumpfun_cli/protocol/instructions.py b/src/pumpfun_cli/protocol/instructions.py index 2689b85..30646cb 100644 --- a/src/pumpfun_cli/protocol/instructions.py +++ b/src/pumpfun_cli/protocol/instructions.py @@ -286,6 +286,7 @@ def build_create_instructions( symbol: str, uri: str, is_mayhem: bool = False, + is_cashback: bool = False, token_program: Pubkey = TOKEN_2022_PROGRAM, ) -> list[Instruction]: """Build create_v2 token instruction for pump.fun (Token2022). @@ -307,6 +308,9 @@ def build_create_instructions( assoc_bc = derive_associated_bonding_curve(mint, bonding_curve, token_program) mint_auth = _derive_mint_authority() + mayhem_state = derive_mayhem_state(mint) + mayhem_token_vault = derive_mayhem_token_vault(mint) + create_accounts = [ AccountMeta(pubkey=mint, is_signer=True, is_writable=True), AccountMeta(pubkey=mint_auth, is_signer=False, is_writable=False), @@ -317,21 +321,15 @@ def build_create_instructions( AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), AccountMeta(pubkey=TOKEN_2022_PROGRAM, is_signer=False, is_writable=False), AccountMeta(pubkey=ASSOCIATED_TOKEN_PROGRAM, is_signer=False, is_writable=False), + # Mayhem accounts are always required by create_v2 per IDL, + # regardless of is_mayhem_mode flag value. + AccountMeta(pubkey=MAYHEM_PROGRAM_ID, is_signer=False, is_writable=True), + AccountMeta(pubkey=MAYHEM_GLOBAL_PARAMS, is_signer=False, is_writable=False), + AccountMeta(pubkey=MAYHEM_SOL_VAULT, is_signer=False, is_writable=True), + AccountMeta(pubkey=mayhem_state, is_signer=False, is_writable=True), + AccountMeta(pubkey=mayhem_token_vault, is_signer=False, is_writable=True), ] - if is_mayhem: - mayhem_state = derive_mayhem_state(mint) - mayhem_token_vault = derive_mayhem_token_vault(mint) - create_accounts.extend( - [ - AccountMeta(pubkey=MAYHEM_PROGRAM_ID, is_signer=False, is_writable=True), - AccountMeta(pubkey=MAYHEM_GLOBAL_PARAMS, is_signer=False, is_writable=False), - AccountMeta(pubkey=MAYHEM_SOL_VAULT, is_signer=False, is_writable=True), - AccountMeta(pubkey=mayhem_state, is_signer=False, is_writable=True), - AccountMeta(pubkey=mayhem_token_vault, is_signer=False, is_writable=True), - ] - ) - create_accounts.extend( [ AccountMeta(pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False), @@ -348,6 +346,7 @@ def build_create_instructions( + _encode_borsh_string(uri) + bytes(user) # creator arg + struct.pack(" Date: Wed, 18 Mar 2026 17:15:14 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20address=20CodeRabbit=20review=20?= =?UTF-8?q?=E2=80=94=20writable=20flag,=20account=20layout=20tests,=20para?= =?UTF-8?q?metrize=20surfpool?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Set MAYHEM_PROGRAM_ID to is_writable=False in create_v2 (read-only per IDL) - Add account count + position assertions to create_v2 instruction tests - Parametrize 8 surfpool launch tests into single matrix test Co-Authored-By: Claude Opus 4.6 (1M context) --- src/pumpfun_cli/protocol/instructions.py | 2 +- tests/test_protocol/test_instructions.py | 30 ++- tests/test_surfpool/test_launch.py | 222 ++++------------------- 3 files changed, 60 insertions(+), 194 deletions(-) diff --git a/src/pumpfun_cli/protocol/instructions.py b/src/pumpfun_cli/protocol/instructions.py index 30646cb..35b0606 100644 --- a/src/pumpfun_cli/protocol/instructions.py +++ b/src/pumpfun_cli/protocol/instructions.py @@ -323,7 +323,7 @@ def build_create_instructions( AccountMeta(pubkey=ASSOCIATED_TOKEN_PROGRAM, is_signer=False, is_writable=False), # Mayhem accounts are always required by create_v2 per IDL, # regardless of is_mayhem_mode flag value. - AccountMeta(pubkey=MAYHEM_PROGRAM_ID, is_signer=False, is_writable=True), + AccountMeta(pubkey=MAYHEM_PROGRAM_ID, is_signer=False, is_writable=False), AccountMeta(pubkey=MAYHEM_GLOBAL_PARAMS, is_signer=False, is_writable=False), AccountMeta(pubkey=MAYHEM_SOL_VAULT, is_signer=False, is_writable=True), AccountMeta(pubkey=mayhem_state, is_signer=False, is_writable=True), diff --git a/tests/test_protocol/test_instructions.py b/tests/test_protocol/test_instructions.py index ac94057..810b113 100644 --- a/tests/test_protocol/test_instructions.py +++ b/tests/test_protocol/test_instructions.py @@ -3,14 +3,23 @@ from solders.pubkey import Pubkey from pumpfun_cli.protocol.address import derive_associated_bonding_curve, derive_bonding_curve -from pumpfun_cli.protocol.contracts import BUY_EXACT_SOL_IN_DISCRIMINATOR +from pumpfun_cli.protocol.contracts import ( + BUY_EXACT_SOL_IN_DISCRIMINATOR, + MAYHEM_GLOBAL_PARAMS, + MAYHEM_PROGRAM_ID, + MAYHEM_SOL_VAULT, +) from pumpfun_cli.protocol.idl_parser import IDLParser from pumpfun_cli.protocol.instructions import ( build_buy_exact_sol_in_instructions, build_buy_instructions, + build_create_instructions, build_sell_instructions, ) +# create_v2 must always have exactly 16 accounts (including mayhem accounts). +_EXPECTED_CREATE_V2_ACCOUNTS = 16 + IDL_PATH = Path(__file__).parent.parent.parent / "idl" / "pump_fun_idl.json" _MINT = Pubkey.from_string("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v") _USER = Pubkey.from_string("11111111111111111111111111111112") @@ -85,8 +94,6 @@ def test_buy_exact_sol_in_discriminator(): def test_create_instructions_cashback_false(): """create_v2 with is_cashback=False encodes OptionBool as 0x00.""" - from pumpfun_cli.protocol.instructions import build_create_instructions - idl = IDLParser(str(IDL_PATH)) ixs = build_create_instructions( idl=idl, @@ -100,6 +107,11 @@ def test_create_instructions_cashback_false(): ) assert len(ixs) == 1 create_ix = ixs[0] + # Lock account layout to prevent AccountNotEnoughKeys regressions + assert len(create_ix.accounts) == _EXPECTED_CREATE_V2_ACCOUNTS + assert create_ix.accounts[9].pubkey == MAYHEM_PROGRAM_ID + assert create_ix.accounts[10].pubkey == MAYHEM_GLOBAL_PARAMS + assert create_ix.accounts[11].pubkey == MAYHEM_SOL_VAULT # Last byte should be 0x00 (is_cashback_enabled = false) assert create_ix.data[-1:] == b"\x00" # Second-to-last byte is is_mayhem_mode = false @@ -108,8 +120,6 @@ def test_create_instructions_cashback_false(): def test_create_instructions_cashback_true(): """create_v2 with is_cashback=True encodes OptionBool as 0x01.""" - from pumpfun_cli.protocol.instructions import build_create_instructions - idl = IDLParser(str(IDL_PATH)) ixs = build_create_instructions( idl=idl, @@ -123,6 +133,10 @@ def test_create_instructions_cashback_true(): ) assert len(ixs) == 1 create_ix = ixs[0] + assert len(create_ix.accounts) == _EXPECTED_CREATE_V2_ACCOUNTS + assert create_ix.accounts[9].pubkey == MAYHEM_PROGRAM_ID + assert create_ix.accounts[10].pubkey == MAYHEM_GLOBAL_PARAMS + assert create_ix.accounts[11].pubkey == MAYHEM_SOL_VAULT # Last byte should be 0x01 (is_cashback_enabled = true) assert create_ix.data[-1:] == b"\x01" # Second-to-last byte is is_mayhem_mode = false @@ -131,8 +145,6 @@ def test_create_instructions_cashback_true(): def test_create_instructions_mayhem_and_cashback(): """create_v2 with both is_mayhem=True and is_cashback=True.""" - from pumpfun_cli.protocol.instructions import build_create_instructions - idl = IDLParser(str(IDL_PATH)) ixs = build_create_instructions( idl=idl, @@ -146,6 +158,10 @@ def test_create_instructions_mayhem_and_cashback(): ) assert len(ixs) == 1 create_ix = ixs[0] + assert len(create_ix.accounts) == _EXPECTED_CREATE_V2_ACCOUNTS + assert create_ix.accounts[9].pubkey == MAYHEM_PROGRAM_ID + assert create_ix.accounts[10].pubkey == MAYHEM_GLOBAL_PARAMS + assert create_ix.accounts[11].pubkey == MAYHEM_SOL_VAULT # Last byte = cashback true, second-to-last = mayhem true assert create_ix.data[-1:] == b"\x01" assert create_ix.data[-2:-1] == b"\x01" diff --git a/tests/test_surfpool/test_launch.py b/tests/test_surfpool/test_launch.py index 2f5db4b..fd31f16 100644 --- a/tests/test_surfpool/test_launch.py +++ b/tests/test_surfpool/test_launch.py @@ -24,208 +24,58 @@ return_value=FAKE_URI, ) - -# ── mayhem=False, cashback=False (default) ────────────────────────────── - - -@pytest.mark.asyncio -@_UPLOAD_PATCH -async def test_launch_default( - _mock_upload, surfpool_rpc, funded_keypair, test_keystore, test_password -): - """Launch with defaults: no mayhem, no cashback.""" - result = await launch_token( - rpc_url=surfpool_rpc, - keystore_path=str(test_keystore), - password=test_password, - name="Default", - ticker="DFLT", - description="default launch", - ) - - assert "error" not in result, f"Launch failed: {result}" - assert result["action"] == "launch" - assert result["is_cashback"] is False - assert result["signature"] - - info = await get_token_info(surfpool_rpc, result["mint"]) - assert "error" not in info, f"Token not found: {info}" - assert info["graduated"] is False - - -# ── mayhem=False, cashback=True ───────────────────────────────────────── +_LAUNCH_MATRIX = [ + pytest.param(False, False, None, id="default"), + pytest.param(False, True, None, id="cashback"), + pytest.param(True, False, None, id="mayhem"), + pytest.param(True, True, None, id="mayhem+cashback"), + pytest.param(False, False, 0.001, id="default+buy"), + pytest.param(False, True, 0.001, id="cashback+buy"), + pytest.param(True, False, 0.001, id="mayhem+buy"), + pytest.param(True, True, 0.001, id="mayhem+cashback+buy"), +] @pytest.mark.asyncio +@pytest.mark.parametrize("is_mayhem,is_cashback,initial_buy_sol", _LAUNCH_MATRIX) @_UPLOAD_PATCH -async def test_launch_cashback( - _mock_upload, surfpool_rpc, funded_keypair, test_keystore, test_password +async def test_launch( + _mock_upload, + surfpool_rpc, + funded_keypair, + test_keystore, + test_password, + is_mayhem, + is_cashback, + initial_buy_sol, ): - """Launch with cashback enabled, no mayhem.""" - result = await launch_token( - rpc_url=surfpool_rpc, - keystore_path=str(test_keystore), - password=test_password, - name="Cashback", - ticker="CSHB", - description="cashback launch", - is_cashback=True, - ) - - assert "error" not in result, f"Launch failed: {result}" - assert result["is_cashback"] is True - assert result["signature"] - - info = await get_token_info(surfpool_rpc, result["mint"]) - assert "error" not in info, f"Token not found: {info}" - assert info["graduated"] is False - - -# ── mayhem=True, cashback=False ───────────────────────────────────────── + """Launch token with given mayhem/cashback/buy combination.""" + kwargs = {} + if is_mayhem: + kwargs["is_mayhem"] = True + if is_cashback: + kwargs["is_cashback"] = True + if initial_buy_sol is not None: + kwargs["initial_buy_sol"] = initial_buy_sol - -@pytest.mark.asyncio -@_UPLOAD_PATCH -async def test_launch_mayhem( - _mock_upload, surfpool_rpc, funded_keypair, test_keystore, test_password -): - """Launch with mayhem enabled, no cashback.""" result = await launch_token( rpc_url=surfpool_rpc, keystore_path=str(test_keystore), password=test_password, - name="Mayhem", - ticker="MYHM", - description="mayhem launch", - is_mayhem=True, + name="Test", + ticker="TST", + description="parametrized launch", + **kwargs, ) assert "error" not in result, f"Launch failed: {result}" - assert result["is_cashback"] is False + assert result["action"] == "launch" + assert result["is_cashback"] is is_cashback assert result["signature"] - info = await get_token_info(surfpool_rpc, result["mint"]) - assert "error" not in info, f"Token not found: {info}" - assert info["graduated"] is False - - -# ── mayhem=True, cashback=True ────────────────────────────────────────── - - -@pytest.mark.asyncio -@_UPLOAD_PATCH -async def test_launch_mayhem_cashback( - _mock_upload, surfpool_rpc, funded_keypair, test_keystore, test_password -): - """Launch with both mayhem and cashback enabled.""" - result = await launch_token( - rpc_url=surfpool_rpc, - keystore_path=str(test_keystore), - password=test_password, - name="MayhemCB", - ticker="MHCB", - description="mayhem + cashback launch", - is_mayhem=True, - is_cashback=True, - ) - - assert "error" not in result, f"Launch failed: {result}" - assert result["is_cashback"] is True - assert result["signature"] + if initial_buy_sol is not None: + assert result["initial_buy_sol"] == initial_buy_sol info = await get_token_info(surfpool_rpc, result["mint"]) assert "error" not in info, f"Token not found: {info}" assert info["graduated"] is False - - -# ── with initial buy ──────────────────────────────────────────────────── - - -@pytest.mark.asyncio -@_UPLOAD_PATCH -async def test_launch_default_with_buy( - _mock_upload, surfpool_rpc, funded_keypair, test_keystore, test_password -): - """Launch with initial buy, no mayhem, no cashback.""" - result = await launch_token( - rpc_url=surfpool_rpc, - keystore_path=str(test_keystore), - password=test_password, - name="DefBuy", - ticker="DBUY", - description="default + buy", - initial_buy_sol=0.001, - ) - - assert "error" not in result, f"Launch failed: {result}" - assert result["initial_buy_sol"] == 0.001 - assert result["signature"] - - -@pytest.mark.asyncio -@_UPLOAD_PATCH -async def test_launch_cashback_with_buy( - _mock_upload, surfpool_rpc, funded_keypair, test_keystore, test_password -): - """Launch cashback token with initial buy.""" - result = await launch_token( - rpc_url=surfpool_rpc, - keystore_path=str(test_keystore), - password=test_password, - name="CashBuy", - ticker="CBUY", - description="cashback + buy", - is_cashback=True, - initial_buy_sol=0.001, - ) - - assert "error" not in result, f"Launch failed: {result}" - assert result["is_cashback"] is True - assert result["initial_buy_sol"] == 0.001 - assert result["signature"] - - -@pytest.mark.asyncio -@_UPLOAD_PATCH -async def test_launch_mayhem_with_buy( - _mock_upload, surfpool_rpc, funded_keypair, test_keystore, test_password -): - """Launch mayhem token with initial buy.""" - result = await launch_token( - rpc_url=surfpool_rpc, - keystore_path=str(test_keystore), - password=test_password, - name="MyhBuy", - ticker="MBUY", - description="mayhem + buy", - is_mayhem=True, - initial_buy_sol=0.001, - ) - - assert "error" not in result, f"Launch failed: {result}" - assert result["initial_buy_sol"] == 0.001 - assert result["signature"] - - -@pytest.mark.asyncio -@_UPLOAD_PATCH -async def test_launch_mayhem_cashback_with_buy( - _mock_upload, surfpool_rpc, funded_keypair, test_keystore, test_password -): - """Launch mayhem + cashback token with initial buy.""" - result = await launch_token( - rpc_url=surfpool_rpc, - keystore_path=str(test_keystore), - password=test_password, - name="MhCbBuy", - ticker="MCBB", - description="mayhem + cashback + buy", - is_mayhem=True, - is_cashback=True, - initial_buy_sol=0.001, - ) - - assert "error" not in result, f"Launch failed: {result}" - assert result["is_cashback"] is True - assert result["initial_buy_sol"] == 0.001 - assert result["signature"]