mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-11-02 06:16:10 +00:00
329 lines
12 KiB
Python
329 lines
12 KiB
Python
"""Create a pump.fun coin with create_v2, without buying any of it.
|
|
|
|
WARNING: this submits a real transaction and spends real funds (transaction fees
|
|
and account rent — no coins are bought).
|
|
|
|
Usage:
|
|
uv run cookbook/pumpfun/trade/pumpfun_create_token_v2.py
|
|
uv run cookbook/pumpfun/trade/pumpfun_create_token_v2.py --name "My Coin" --symbol MINE
|
|
uv run cookbook/pumpfun/trade/pumpfun_create_token_v2.py --mayhem --holder-reward
|
|
|
|
`pumpfun_create_and_buy_token_v2.py` does this and then buys the coin in a second
|
|
transaction. This is the create half on its own: everything about a coin —
|
|
Token-2022, mayhem mode, holder rewards, the quote asset — is fixed here and
|
|
cannot be changed afterwards.
|
|
|
|
- The mint is a **keypair you generate**, not something pump.fun hands back. It
|
|
signs the create transaction alongside your wallet.
|
|
- `create_v2` mints under **Token-2022**, so the associated bonding curve is a
|
|
Token-2022 ATA. Deriving it with SPL Token gives a valid-looking address that
|
|
does not exist on chain.
|
|
|
|
The `extend_account` instruction that follows the create is what makes the coin
|
|
visible on pump.fun's own frontend. The coin trades without it.
|
|
"""
|
|
|
|
import argparse
|
|
import asyncio
|
|
import os
|
|
import struct
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Final
|
|
|
|
# solana_transaction_status.py lives in cookbook/solana/.
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "solana"))
|
|
|
|
import base58
|
|
import solana_transaction_status as tx_status
|
|
from dotenv import load_dotenv
|
|
from solana.rpc.async_api import AsyncClient
|
|
from solana.rpc.commitment import Confirmed
|
|
from solana.rpc.core import TxOptsModel
|
|
from solders.compute_budget import set_compute_unit_limit, set_compute_unit_price
|
|
from solders.instruction import AccountMeta, Instruction
|
|
from solders.keypair import Keypair
|
|
from solders.message import MessageV0
|
|
from solders.pubkey import Pubkey
|
|
from solders.transaction import VersionedTransaction
|
|
from spl.token.instructions import get_associated_token_address
|
|
|
|
load_dotenv()
|
|
|
|
RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
|
|
PRIVATE_KEY = os.environ.get("SOLANA_PRIVATE_KEY")
|
|
|
|
PUMP_PROGRAM: Final[Pubkey] = Pubkey.from_string(
|
|
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
|
|
)
|
|
PUMP_GLOBAL: Final[Pubkey] = Pubkey.from_string(
|
|
"4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf"
|
|
)
|
|
PUMP_EVENT_AUTHORITY: Final[Pubkey] = Pubkey.from_string(
|
|
"Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1"
|
|
)
|
|
PUMP_MINT_AUTHORITY: Final[Pubkey] = Pubkey.from_string(
|
|
"TSLvdd1pWpHVjahSpsvCXUbgwsL3JAcvokwaKt1eokM"
|
|
)
|
|
SYSTEM_PROGRAM: Final[Pubkey] = Pubkey.from_string("11111111111111111111111111111111")
|
|
TOKEN_2022_PROGRAM: Final[Pubkey] = Pubkey.from_string(
|
|
"TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
|
|
)
|
|
ASSOCIATED_TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string(
|
|
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
|
)
|
|
|
|
# Mayhem mode is a separate program with its own accounts, appended to the
|
|
# create_v2 account list only when the coin opts into it.
|
|
MAYHEM_PROGRAM: Final[Pubkey] = Pubkey.from_string(
|
|
"MAyhSmzXzV1pTf7LsNkrNwkWKTo4ougAJ1PPg47MD4e"
|
|
)
|
|
MAYHEM_GLOBAL_PARAMS: Final[Pubkey] = Pubkey.from_string(
|
|
"13ec7XdrjF3h3YcqBTFDSReRcUFwbCnJaAQspM4j6DDJ"
|
|
)
|
|
MAYHEM_SOL_VAULT: Final[Pubkey] = Pubkey.from_string(
|
|
"BwWK17cbHxwWBKZkUYvzxLcNQ1YVyaFezduWbtm2de6s"
|
|
)
|
|
|
|
# First 8 bytes of sha256("global:<instruction name>").
|
|
CREATE_V2_DISCRIMINATOR: Final[bytes] = bytes([214, 144, 76, 236, 95, 139, 49, 180])
|
|
EXTEND_ACCOUNT_DISCRIMINATOR: Final[bytes] = bytes(
|
|
[234, 102, 194, 203, 150, 72, 62, 229]
|
|
)
|
|
|
|
COMPUTE_UNIT_LIMIT: Final[int] = 350_000
|
|
PRIORITY_FEE_MICROLAMPORTS: Final[int] = 37_037
|
|
|
|
# Defaults for the command line below, not fixed settings.
|
|
DEFAULT_MAYHEM = False
|
|
DEFAULT_HOLDER_REWARD = False
|
|
|
|
|
|
def encode_string(value: str) -> bytes:
|
|
"""Encode a string the way Borsh does: a u32 length, then the bytes.
|
|
|
|
Args:
|
|
value: The string to encode
|
|
|
|
Returns:
|
|
Length-prefixed UTF-8 bytes
|
|
"""
|
|
encoded = value.encode("utf-8")
|
|
return struct.pack("<I", len(encoded)) + encoded
|
|
|
|
|
|
def build_create_v2_instruction( # noqa: PLR0913
|
|
mint: Pubkey,
|
|
user: Pubkey,
|
|
name: str,
|
|
symbol: str,
|
|
uri: str,
|
|
*,
|
|
is_mayhem_mode: bool,
|
|
is_holder_reward: bool,
|
|
) -> Instruction:
|
|
"""Build the create_v2 instruction.
|
|
|
|
Args:
|
|
mint: The new coin's mint (a keypair you generate)
|
|
user: Wallet paying for and creating the coin
|
|
name: Coin name
|
|
symbol: Coin ticker
|
|
uri: Metadata URI
|
|
is_mayhem_mode: Whether the coin opts into mayhem mode
|
|
is_holder_reward: Whether the creator fee is set aside for holders
|
|
|
|
Returns:
|
|
The create_v2 instruction
|
|
"""
|
|
bonding_curve = Pubkey.find_program_address(
|
|
[b"bonding-curve", bytes(mint)], PUMP_PROGRAM
|
|
)[0]
|
|
# An ordinary ATA, but under Token-2022 because create_v2 mints there.
|
|
associated_bonding_curve = Pubkey.find_program_address(
|
|
[bytes(bonding_curve), bytes(TOKEN_2022_PROGRAM), bytes(mint)],
|
|
ASSOCIATED_TOKEN_PROGRAM,
|
|
)[0]
|
|
|
|
accounts = [
|
|
AccountMeta(pubkey=mint, is_signer=True, is_writable=True),
|
|
AccountMeta(pubkey=PUMP_MINT_AUTHORITY, is_signer=False, is_writable=False),
|
|
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
|
|
AccountMeta(pubkey=associated_bonding_curve, is_signer=False, is_writable=True),
|
|
AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False),
|
|
AccountMeta(pubkey=user, is_signer=True, is_writable=True),
|
|
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
|
|
),
|
|
]
|
|
|
|
# The five mayhem accounts are mandatory even on a coin that is not a mayhem
|
|
# coin — the IDL marks none of them optional, and omitting them fails with
|
|
# AnchorError 3005 (AccountNotEnoughKeys) on sol_vault. The is_mayhem_mode
|
|
# argument below, not the account list, is what makes a coin a mayhem coin.
|
|
mayhem_state = Pubkey.find_program_address(
|
|
[b"mayhem-state", bytes(mint)], MAYHEM_PROGRAM
|
|
)[0]
|
|
accounts += [
|
|
AccountMeta(pubkey=MAYHEM_PROGRAM, 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=get_associated_token_address(
|
|
MAYHEM_SOL_VAULT, mint, TOKEN_2022_PROGRAM
|
|
),
|
|
is_signer=False,
|
|
is_writable=True,
|
|
),
|
|
]
|
|
|
|
accounts += [
|
|
AccountMeta(pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False),
|
|
AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False),
|
|
]
|
|
|
|
data = (
|
|
CREATE_V2_DISCRIMINATOR
|
|
+ encode_string(name)
|
|
+ encode_string(symbol)
|
|
+ encode_string(uri)
|
|
+ bytes(user) # creator
|
|
+ struct.pack("<?", is_mayhem_mode)
|
|
)
|
|
|
|
if is_holder_reward:
|
|
# The three trailing args are positional, not independently optional:
|
|
# reaching is_holder_reward means sending is_cashback_enabled and
|
|
# creator_fee_bps first, even though both are unused here. Neither is a
|
|
# discriminated Option — each serializes as its bare inner value, one
|
|
# byte and a little-endian u64.
|
|
#
|
|
# is_cashback_enabled is always False: create_v2 rejects [true] with
|
|
# error 6082 (CashbackDeprecated).
|
|
is_cashback_enabled = False
|
|
creator_fee_bps = 0
|
|
data += struct.pack("<?", is_cashback_enabled)
|
|
data += struct.pack("<Q", creator_fee_bps)
|
|
data += struct.pack("<?", is_holder_reward)
|
|
|
|
return Instruction(PUMP_PROGRAM, data, accounts)
|
|
|
|
|
|
def build_extend_account_instruction(
|
|
bonding_curve: Pubkey, user: Pubkey
|
|
) -> Instruction:
|
|
"""Build the extend_account instruction, which takes no arguments.
|
|
|
|
Args:
|
|
bonding_curve: The coin's bonding curve
|
|
user: Wallet paying for the extra account space
|
|
"""
|
|
accounts = [
|
|
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
|
|
AccountMeta(pubkey=user, is_signer=True, is_writable=True),
|
|
AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False),
|
|
AccountMeta(pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False),
|
|
AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False),
|
|
]
|
|
return Instruction(PUMP_PROGRAM, EXTEND_ACCOUNT_DISCRIMINATOR, accounts)
|
|
|
|
|
|
async def create(
|
|
name: str, symbol: str, uri: str, *, mayhem: bool, holder_reward: bool
|
|
) -> None:
|
|
"""Create one coin and print its mint.
|
|
|
|
Args:
|
|
name: Coin name
|
|
symbol: Coin ticker
|
|
uri: Metadata URI
|
|
mayhem: Whether to opt into mayhem mode
|
|
holder_reward: Whether the creator fee goes to holders
|
|
"""
|
|
payer = Keypair.from_bytes(base58.b58decode(PRIVATE_KEY))
|
|
mint_keypair = Keypair()
|
|
mint = mint_keypair.pubkey()
|
|
bonding_curve = Pubkey.find_program_address(
|
|
[b"bonding-curve", bytes(mint)], PUMP_PROGRAM
|
|
)[0]
|
|
|
|
print(f"Mint: {mint}")
|
|
print(f"Curve: {bonding_curve}")
|
|
print(f"Creator: {payer.pubkey()}")
|
|
print(f"Name: {name} ({symbol})")
|
|
print(f"Mayhem: {mayhem} Holder reward: {holder_reward}")
|
|
|
|
instructions = [
|
|
set_compute_unit_limit(COMPUTE_UNIT_LIMIT),
|
|
set_compute_unit_price(PRIORITY_FEE_MICROLAMPORTS),
|
|
build_create_v2_instruction(
|
|
mint,
|
|
payer.pubkey(),
|
|
name,
|
|
symbol,
|
|
uri,
|
|
is_mayhem_mode=mayhem,
|
|
is_holder_reward=holder_reward,
|
|
),
|
|
build_extend_account_instruction(bonding_curve, payer.pubkey()),
|
|
]
|
|
|
|
async with AsyncClient(RPC_ENDPOINT) as client:
|
|
blockhash = (await client.get_latest_blockhash()).value.blockhash
|
|
# The mint signs too — it is a brand-new account being created.
|
|
transaction = VersionedTransaction(
|
|
MessageV0.try_compile(payer.pubkey(), instructions, [], blockhash),
|
|
[payer, mint_keypair],
|
|
)
|
|
signature = (
|
|
await client.send_transaction(
|
|
transaction,
|
|
opts=TxOptsModel(skip_preflight=True, preflight_commitment=Confirmed),
|
|
)
|
|
).value
|
|
|
|
print(f"\nSent: https://explorer.solana.com/tx/{signature}")
|
|
await tx_status.confirm_and_assert(client, signature)
|
|
print("Confirmed")
|
|
print(f"\nBuy it with: uv run cookbook/pumpfun/trade/pumpfun_buy_token_v2.py {mint}")
|
|
|
|
|
|
def main() -> None:
|
|
"""Parse the command line and create the coin."""
|
|
parser = argparse.ArgumentParser(description="Create one pump.fun coin")
|
|
parser.add_argument("--name", default="Test Token", help="Coin name")
|
|
parser.add_argument("--symbol", default="TEST", help="Coin ticker")
|
|
parser.add_argument(
|
|
"--uri", default="https://example.com/token.json", help="Metadata URI"
|
|
)
|
|
parser.add_argument(
|
|
"--mayhem",
|
|
action=argparse.BooleanOptionalAction,
|
|
default=DEFAULT_MAYHEM,
|
|
help=f"Enable mayhem mode (default {DEFAULT_MAYHEM})",
|
|
)
|
|
parser.add_argument(
|
|
"--holder-reward",
|
|
action=argparse.BooleanOptionalAction,
|
|
default=DEFAULT_HOLDER_REWARD,
|
|
help=f"Set the creator fee aside for holders (default {DEFAULT_HOLDER_REWARD})",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
asyncio.run(
|
|
create(
|
|
args.name,
|
|
args.symbol,
|
|
args.uri,
|
|
mayhem=args.mayhem,
|
|
holder_reward=args.holder_reward,
|
|
)
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|