Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 additions & 0 deletions src/pumpfun_cli/commands/launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -49,6 +50,7 @@ def launch(
image,
buy,
mayhem,
cashback,
**overrides,
)
)
Expand All @@ -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")
Comment thread
smypmsa marked this conversation as resolved.
if result.get("initial_buy_sol"):
typer.echo(f" Buy: {result['initial_buy_sol']} SOL")
3 changes: 3 additions & 0 deletions src/pumpfun_cli/core/launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -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}",
Expand Down
25 changes: 12 additions & 13 deletions src/pumpfun_cli/protocol/instructions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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),
Expand All @@ -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),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
]

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),
Expand All @@ -348,6 +346,7 @@ def build_create_instructions(
+ _encode_borsh_string(uri)
+ bytes(user) # creator arg
+ struct.pack("<?", is_mayhem) # is_mayhem_mode: bool
+ struct.pack("<?", is_cashback) # is_cashback_enabled: OptionBool
)

create_ix = Instruction(
Expand Down
8 changes: 8 additions & 0 deletions tests/test_commands/test_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,14 @@ def test_tokens_no_subcommand_shows_help():
assert result.exit_code == 0


def test_launch_help_shows_cashback():
"""Launch --help includes --cashback flag."""
result = runner.invoke(app, ["launch", "--help"])
assert result.exit_code == 0
out = _strip_ansi(result.output)
assert "--cashback" in out


@pytest.mark.parametrize("limit", ["0", "-1"])
@pytest.mark.parametrize(
("subcommand", "extra_args"),
Expand Down
91 changes: 91 additions & 0 deletions tests/test_core/test_launch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""Tests for core/launch.py — cashback flag pass-through."""

from unittest.mock import AsyncMock, patch

import pytest


@pytest.mark.asyncio
async def test_launch_passes_cashback_false(tmp_keystore):
"""launch_token passes is_cashback=False through to build_create_instructions."""
with (
patch(
"pumpfun_cli.core.launch.upload_metadata",
new_callable=AsyncMock,
return_value="https://ipfs.example.com/metadata.json",
),
patch("pumpfun_cli.core.launch.build_create_instructions", wraps=None) as mock_build,
patch("pumpfun_cli.core.launch.build_extend_account_instruction"),
patch(
"pumpfun_cli.core.launch.RpcClient",
) as mock_rpc_cls,
):
mock_client = AsyncMock()
mock_client.send_tx = AsyncMock(return_value="fakesig123")
mock_rpc_cls.return_value = mock_client

# build_create_instructions needs to return a list of instructions
from unittest.mock import MagicMock

mock_ix = MagicMock()
mock_build.return_value = [mock_ix]

from pumpfun_cli.core.launch import launch_token

result = await launch_token(
rpc_url="https://fake.rpc",
keystore_path=tmp_keystore,
password="testpass",
name="TestToken",
ticker="TST",
description="A test token",
is_cashback=False,
)

mock_build.assert_called_once()
call_kwargs = mock_build.call_args
assert call_kwargs.kwargs.get("is_cashback") is False or (
not call_kwargs.kwargs.get("is_cashback", True)
)
assert result["is_cashback"] is False


@pytest.mark.asyncio
async def test_launch_passes_cashback_true(tmp_keystore):
"""launch_token passes is_cashback=True through to build_create_instructions."""
with (
patch(
"pumpfun_cli.core.launch.upload_metadata",
new_callable=AsyncMock,
return_value="https://ipfs.example.com/metadata.json",
),
patch("pumpfun_cli.core.launch.build_create_instructions", wraps=None) as mock_build,
patch("pumpfun_cli.core.launch.build_extend_account_instruction"),
patch(
"pumpfun_cli.core.launch.RpcClient",
) as mock_rpc_cls,
):
mock_client = AsyncMock()
mock_client.send_tx = AsyncMock(return_value="fakesig456")
mock_rpc_cls.return_value = mock_client

from unittest.mock import MagicMock

mock_ix = MagicMock()
mock_build.return_value = [mock_ix]

from pumpfun_cli.core.launch import launch_token

result = await launch_token(
rpc_url="https://fake.rpc",
keystore_path=tmp_keystore,
password="testpass",
name="TestToken",
ticker="TST",
description="A test token",
is_cashback=True,
)

mock_build.assert_called_once()
assert mock_build.call_args.kwargs["is_cashback"] is True
assert result["is_cashback"] is True
68 changes: 68 additions & 0 deletions tests/test_protocol/test_instructions.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,71 @@ def test_buy_exact_sol_in_discriminator():
)
buy_ix = ixs[-1]
assert buy_ix.data[:8] == 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,
mint=_MINT,
user=_USER,
name="Test",
symbol="TST",
uri="https://example.com",
is_mayhem=False,
is_cashback=False,
)
assert len(ixs) == 1
create_ix = ixs[0]
# 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
assert create_ix.data[-2:-1] == b"\x00"


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,
mint=_MINT,
user=_USER,
name="Test",
symbol="TST",
uri="https://example.com",
is_mayhem=False,
is_cashback=True,
)
assert len(ixs) == 1
create_ix = ixs[0]
# 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
assert create_ix.data[-2:-1] == b"\x00"


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,
mint=_MINT,
user=_USER,
name="Test",
symbol="TST",
uri="https://example.com",
is_mayhem=True,
is_cashback=True,
)
assert len(ixs) == 1
create_ix = ixs[0]
# Last byte = cashback true, second-to-last = mayhem true
assert create_ix.data[-1:] == b"\x01"
assert create_ix.data[-2:-1] == b"\x01"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Loading
Loading