repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources16d ago| #1 | """PumpFun Client - Launch and trade tokens on pump.fun""" |
| #2 | |
| #3 | import httpx |
| #4 | import base58 |
| #5 | import struct |
| #6 | import json |
| #7 | from typing import Optional, List |
| #8 | from dataclasses import dataclass |
| #9 | from pathlib import Path |
| #10 | |
| #11 | from solders.keypair import Keypair |
| #12 | from solders.pubkey import Pubkey |
| #13 | from solders.instruction import Instruction, AccountMeta |
| #14 | from solders.transaction import VersionedTransaction |
| #15 | from solders.message import MessageV0 |
| #16 | from solders.hash import Hash |
| #17 | from solders.system_program import ID as SYSTEM_PROGRAM_ID |
| #18 | from solders.sysvar import RENT as SYSVAR_RENT_ID |
| #19 | |
| #20 | |
| #21 | # Program IDs |
| #22 | PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") |
| #23 | PUMP_GLOBAL = Pubkey.from_string("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf") |
| #24 | PUMP_FEE_RECIPIENT = Pubkey.from_string("62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV") |
| #25 | PUMP_EVENT_AUTHORITY = Pubkey.from_string("Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1") |
| #26 | |
| #27 | # Mayhem mode (new token creation) |
| #28 | MAYHEM_PROGRAM_ID = Pubkey.from_string("MAyhSmzXzV1pTf7LsNkrNwkWKTo4ougAJ1PPg47MD4e") |
| #29 | MAYHEM_GLOBAL_PARAMS = Pubkey.from_string("13ec7XdrjF3h3YcqBTFDSReRcUFwbCnJaAQspM4j6DDJ") |
| #30 | MAYHEM_SOL_VAULT = Pubkey.from_string("BwWK17cbHxwWBKZkUYvzxLcNQ1YVyaFezduWbtm2de6s") |
| #31 | |
| #32 | # Token programs |
| #33 | TOKEN_PROGRAM_ID = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") |
| #34 | TOKEN_2022_PROGRAM_ID = Pubkey.from_string("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb") |
| #35 | ASSOCIATED_TOKEN_PROGRAM_ID = Pubkey.from_string("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL") |
| #36 | |
| #37 | # Metaplex |
| #38 | METAPLEX_PROGRAM_ID = Pubkey.from_string("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s") |
| #39 | |
| #40 | # Instruction discriminators (from IDL) |
| #41 | CREATE_DISCRIMINATOR = bytes([24, 30, 200, 40, 5, 28, 7, 119]) |
| #42 | CREATE_V2_DISCRIMINATOR = bytes([31, 240, 17, 64, 245, 233, 167, 192]) |
| #43 | BUY_DISCRIMINATOR = bytes([102, 6, 61, 18, 1, 218, 235, 234]) |
| #44 | SELL_DISCRIMINATOR = bytes([51, 230, 133, 164, 1, 127, 131, 173]) |
| #45 | |
| #46 | # pump.fun API |
| #47 | PUMPFUN_API_BASE = "https://pump.fun/api" |
| #48 | IPFS_UPLOAD_URL = "https://pump.fun/api/ipfs" |
| #49 | |
| #50 | |
| #51 | @dataclass |
| #52 | class TokenMetadata: |
| #53 | """Token metadata for pump.fun""" |
| #54 | name: str |
| #55 | symbol: str |
| #56 | description: str |
| #57 | image_url: Optional[str] = None |
| #58 | twitter: Optional[str] = None |
| #59 | telegram: Optional[str] = None |
| #60 | website: Optional[str] = None |
| #61 | |
| #62 | |
| #63 | @dataclass |
| #64 | class BondingCurveState: |
| #65 | """Bonding curve account state""" |
| #66 | virtual_token_reserves: int |
| #67 | virtual_sol_reserves: int |
| #68 | real_token_reserves: int |
| #69 | real_sol_reserves: int |
| #70 | token_total_supply: int |
| #71 | complete: bool |
| #72 | creator: Pubkey |
| #73 | is_mayhem_mode: bool |
| #74 | |
| #75 | |
| #76 | @dataclass |
| #77 | class CreateTokenResult: |
| #78 | """Result of token creation""" |
| #79 | mint: Pubkey |
| #80 | bonding_curve: Pubkey |
| #81 | signature: str |
| #82 | token_url: str |
| #83 | |
| #84 | |
| #85 | class PumpFunClient: |
| #86 | """Client for pump.fun token launches and trading""" |
| #87 | |
| #88 | def __init__( |
| #89 | self, |
| #90 | rpc_url: str, |
| #91 | private_key: Optional[str] = None, |
| #92 | ): |
| #93 | """ |
| #94 | Initialize PumpFun client. |
| #95 | |
| #96 | Args: |
| #97 | rpc_url: Solana RPC URL |
| #98 | private_key: Wallet private key (base58 encoded) |
| #99 | """ |
| #100 | self.rpc_url = rpc_url |
| #101 | self.keypair = None |
| #102 | |
| #103 | if private_key: |
| #104 | try: |
| #105 | secret_bytes = base58.b58decode(private_key) |
| #106 | self.keypair = Keypair.from_bytes(secret_bytes) |
| #107 | except Exception as e: |
| #108 | raise ValueError(f"Invalid private key: {e}") |
| #109 | |
| #110 | self._http_client = httpx.AsyncClient(timeout=60.0) |
| #111 | self._rpc_client = httpx.AsyncClient( |
| #112 | base_url=rpc_url, |
| #113 | headers={"Content-Type": "application/json"}, |
| #114 | timeout=60.0 |
| #115 | ) |
| #116 | |
| #117 | @property |
| #118 | def wallet_pubkey(self) -> Optional[Pubkey]: |
| #119 | """Get wallet public key""" |
| #120 | if self.keypair: |
| #121 | return self.keypair.pubkey() |
| #122 | return None |
| #123 | |
| #124 | # =================== |
| #125 | # PDA Derivations |
| #126 | # =================== |
| #127 | |
| #128 | def get_mint_authority_pda(self) -> Pubkey: |
| #129 | """Derive mint authority PDA""" |
| #130 | seeds = [b"mint-authority"] |
| #131 | pda, _ = Pubkey.find_program_address(seeds, PUMP_PROGRAM_ID) |
| #132 | return pda |
| #133 | |
| #134 | def get_bonding_curve_pda(self, mint: Pubkey) -> Pubkey: |
| #135 | """Derive bonding curve PDA for a mint""" |
| #136 | seeds = [b"bonding-curve", bytes(mint)] |
| #137 | pda, _ = Pubkey.find_program_address(seeds, PUMP_PROGRAM_ID) |
| #138 | return pda |
| #139 | |
| #140 | def get_metadata_pda(self, mint: Pubkey) -> Pubkey: |
| #141 | """Derive metadata PDA for a mint""" |
| #142 | seeds = [b"metadata", bytes(METAPLEX_PROGRAM_ID), bytes(mint)] |
| #143 | pda, _ = Pubkey.find_program_address(seeds, METAPLEX_PROGRAM_ID) |
| #144 | return pda |
| #145 | |
| #146 | def get_event_authority_pda(self) -> Pubkey: |
| #147 | """Derive event authority PDA""" |
| #148 | seeds = [b"__event_authority"] |
| #149 | pda, _ = Pubkey.find_program_address(seeds, PUMP_PROGRAM_ID) |
| #150 | return pda |
| #151 | |
| #152 | def get_mayhem_state_pda(self, mint: Pubkey) -> Pubkey: |
| #153 | """Derive mayhem state PDA for create_v2""" |
| #154 | seeds = [b"mayhem-state", bytes(mint)] |
| #155 | pda, _ = Pubkey.find_program_address(seeds, MAYHEM_PROGRAM_ID) |
| #156 | return pda |
| #157 | |
| #158 | def get_associated_token_address( |
| #159 | self, |
| #160 | owner: Pubkey, |
| #161 | mint: Pubkey, |
| #162 | token_program: Pubkey = TOKEN_PROGRAM_ID |
| #163 | ) -> Pubkey: |
| #164 | """Get associated token address""" |
| #165 | seeds = [bytes(owner), bytes(token_program), bytes(mint)] |
| #166 | pda, _ = Pubkey.find_program_address(seeds, ASSOCIATED_TOKEN_PROGRAM_ID) |
| #167 | return pda |
| #168 | |
| #169 | # =================== |
| #170 | # RPC Methods |
| #171 | # =================== |
| #172 | |
| #173 | async def _rpc_request(self, method: str, params: list) -> dict: |
| #174 | """Make RPC request""" |
| #175 | payload = { |
| #176 | "jsonrpc": "2.0", |
| #177 | "id": 1, |
| #178 | "method": method, |
| #179 | "params": params, |
| #180 | } |
| #181 | response = await self._rpc_client.post("", json=payload) |
| #182 | response.raise_for_status() |
| #183 | result = response.json() |
| #184 | if "error" in result: |
| #185 | raise Exception(f"RPC error: {result['error']}") |
| #186 | return result.get("result") |
| #187 | |
| #188 | async def get_latest_blockhash(self) -> Hash: |
| #189 | """Get latest blockhash""" |
| #190 | result = await self._rpc_request("getLatestBlockhash", [{"commitment": "confirmed"}]) |
| #191 | return Hash.from_string(result["value"]["blockhash"]) |
| #192 | |
| #193 | async def get_account_info(self, pubkey: Pubkey) -> Optional[dict]: |
| #194 | """Get account info""" |
| #195 | result = await self._rpc_request( |
| #196 | "getAccountInfo", |
| #197 | [str(pubkey), {"encoding": "base64", "commitment": "confirmed"}] |
| #198 | ) |
| #199 | return result.get("value") |
| #200 | |
| #201 | async def send_transaction(self, tx: VersionedTransaction) -> str: |
| #202 | """Send and confirm transaction""" |
| #203 | tx_bytes = bytes(tx) |
| #204 | tx_base64 = base58.b58encode(tx_bytes).decode() |
| #205 | |
| #206 | result = await self._rpc_request( |
| #207 | "sendTransaction", |
| #208 | [tx_base64, {"encoding": "base58", "skipPreflight": False}] |
| #209 | ) |
| #210 | return result |
| #211 | |
| #212 | async def get_bonding_curve_state(self, mint: Pubkey) -> Optional[BondingCurveState]: |
| #213 | """Get bonding curve state for a mint""" |
| #214 | bonding_curve = self.get_bonding_curve_pda(mint) |
| #215 | account_info = await self.get_account_info(bonding_curve) |
| #216 | |
| #217 | if not account_info or not account_info.get("data"): |
| #218 | return None |
| #219 | |
| #220 | import base64 |
| #221 | data = base64.b64decode(account_info["data"][0]) |
| #222 | |
| #223 | # Skip 8-byte discriminator |
| #224 | data = data[8:] |
| #225 | |
| #226 | # Parse struct (all u64 except bool and pubkey) |
| #227 | virtual_token_reserves = struct.unpack("<Q", data[0:8])[0] |
| #228 | virtual_sol_reserves = struct.unpack("<Q", data[8:16])[0] |
| #229 | real_token_reserves = struct.unpack("<Q", data[16:24])[0] |
| #230 | real_sol_reserves = struct.unpack("<Q", data[24:32])[0] |
| #231 | token_total_supply = struct.unpack("<Q", data[32:40])[0] |
| #232 | complete = data[40] == 1 |
| #233 | creator = Pubkey.from_bytes(data[41:73]) |
| #234 | is_mayhem_mode = data[73] == 1 if len(data) > 73 else False |
| #235 | |
| #236 | return BondingCurveState( |
| #237 | virtual_token_reserves=virtual_token_reserves, |
| #238 | virtual_sol_reserves=virtual_sol_reserves, |
| #239 | real_token_reserves=real_token_reserves, |
| #240 | real_sol_reserves=real_sol_reserves, |
| #241 | token_total_supply=token_total_supply, |
| #242 | complete=complete, |
| #243 | creator=creator, |
| #244 | is_mayhem_mode=is_mayhem_mode, |
| #245 | ) |
| #246 | |
| #247 | # =================== |
| #248 | # Metadata Upload |
| #249 | # =================== |
| #250 | |
| #251 | async def upload_metadata( |
| #252 | self, |
| #253 | metadata: TokenMetadata, |
| #254 | image_path: Optional[str] = None, |
| #255 | ) -> str: |
| #256 | """ |
| #257 | Upload token metadata to pump.fun IPFS. |
| #258 | |
| #259 | Args: |
| #260 | metadata: Token metadata |
| #261 | image_path: Path to local image file (optional) |
| #262 | |
| #263 | Returns: |
| #264 | IPFS URI for metadata |
| #265 | """ |
| #266 | # If we have a local image, upload it first |
| #267 | if image_path: |
| #268 | image_url = await self._upload_image(image_path) |
| #269 | metadata.image_url = image_url |
| #270 | |
| #271 | # Build metadata JSON |
| #272 | metadata_json = { |
| #273 | "name": metadata.name, |
| #274 | "symbol": metadata.symbol, |
| #275 | "description": metadata.description, |
| #276 | } |
| #277 | |
| #278 | if metadata.image_url: |
| #279 | metadata_json["image"] = metadata.image_url |
| #280 | if metadata.twitter: |
| #281 | metadata_json["twitter"] = metadata.twitter |
| #282 | if metadata.telegram: |
| #283 | metadata_json["telegram"] = metadata.telegram |
| #284 | if metadata.website: |
| #285 | metadata_json["website"] = metadata.website |
| #286 | |
| #287 | # Upload to pump.fun IPFS |
| #288 | response = await self._http_client.post( |
| #289 | IPFS_UPLOAD_URL, |
| #290 | json=metadata_json, |
| #291 | ) |
| #292 | response.raise_for_status() |
| #293 | result = response.json() |
| #294 | |
| #295 | return result.get("metadataUri", result.get("uri")) |
| #296 | |
| #297 | async def _upload_image(self, image_path: str) -> str: |
| #298 | """Upload image to pump.fun""" |
| #299 | path = Path(image_path) |
| #300 | if not path.exists(): |
| #301 | raise FileNotFoundError(f"Image not found: {image_path}") |
| #302 | |
| #303 | # Determine mime type |
| #304 | mime_types = { |
| #305 | ".png": "image/png", |
| #306 | ".jpg": "image/jpeg", |
| #307 | ".jpeg": "image/jpeg", |
| #308 | ".gif": "image/gif", |
| #309 | ".webp": "image/webp", |
| #310 | } |
| #311 | mime_type = mime_types.get(path.suffix.lower(), "application/octet-stream") |
| #312 | |
| #313 | with open(path, "rb") as f: |
| #314 | files = {"file": (path.name, f, mime_type)} |
| #315 | response = await self._http_client.post( |
| #316 | f"{PUMPFUN_API_BASE}/ipfs", |
| #317 | files=files, |
| #318 | ) |
| #319 | |
| #320 | response.raise_for_status() |
| #321 | result = response.json() |
| #322 | return result.get("imageUri", result.get("uri")) |
| #323 | |
| #324 | # =================== |
| #325 | # Token Creation |
| #326 | # =================== |
| #327 | |
| #328 | async def create_token( |
| #329 | self, |
| #330 | name: str, |
| #331 | symbol: str, |
| #332 | description: str, |
| #333 | image_url: Optional[str] = None, |
| #334 | image_path: Optional[str] = None, |
| #335 | twitter: Optional[str] = None, |
| #336 | telegram: Optional[str] = None, |
| #337 | website: Optional[str] = None, |
| #338 | initial_buy_sol: float = 0.0, |
| #339 | ) -> CreateTokenResult: |
| #340 | """ |
| #341 | Create a new token on pump.fun. |
| #342 | |
| #343 | Args: |
| #344 | name: Token name |
| #345 | symbol: Token symbol (ticker) |
| #346 | description: Token description |
| #347 | image_url: URL to token image |
| #348 | image_path: Path to local image file (alternative to image_url) |
| #349 | twitter: Twitter URL |
| #350 | telegram: Telegram URL |
| #351 | website: Website URL |
| #352 | initial_buy_sol: Initial buy amount in SOL (0 = no initial buy) |
| #353 | |
| #354 | Returns: |
| #355 | CreateTokenResult with mint, bonding curve, and signature |
| #356 | """ |
| #357 | if not self.keypair: |
| #358 | raise ValueError("Keypair required for token creation") |
| #359 | |
| #360 | # Create metadata |
| #361 | metadata = TokenMetadata( |
| #362 | name=name, |
| #363 | symbol=symbol.upper().replace("$", ""), |
| #364 | description=description, |
| #365 | image_url=image_url, |
| #366 | twitter=twitter, |
| #367 | telegram=telegram, |
| #368 | website=website, |
| #369 | ) |
| #370 | |
| #371 | # Upload metadata |
| #372 | metadata_uri = await self.upload_metadata(metadata, image_path) |
| #373 | |
| #374 | # Generate mint keypair |
| #375 | mint_keypair = Keypair() |
| #376 | mint = mint_keypair.pubkey() |
| #377 | |
| #378 | # Derive PDAs |
| #379 | mint_authority = self.get_mint_authority_pda() |
| #380 | bonding_curve = self.get_bonding_curve_pda(mint) |
| #381 | associated_bonding_curve = self.get_associated_token_address(bonding_curve, mint) |
| #382 | metadata_pda = self.get_metadata_pda(mint) |
| #383 | event_authority = self.get_event_authority_pda() |
| #384 | |
| #385 | # Build create instruction data |
| #386 | name_bytes = name.encode("utf-8") |
| #387 | symbol_bytes = symbol.upper().replace("$", "").encode("utf-8") |
| #388 | uri_bytes = metadata_uri.encode("utf-8") |
| #389 | |
| #390 | # Serialize: discriminator + strings (each prefixed with 4-byte length) |
| #391 | data = CREATE_DISCRIMINATOR |
| #392 | data += struct.pack("<I", len(name_bytes)) + name_bytes |
| #393 | data += struct.pack("<I", len(symbol_bytes)) + symbol_bytes |
| #394 | data += struct.pack("<I", len(uri_bytes)) + uri_bytes |
| #395 | |
| #396 | # Build accounts |
| #397 | accounts = [ |
| #398 | AccountMeta(mint, is_signer=True, is_writable=True), |
| #399 | AccountMeta(mint_authority, is_signer=False, is_writable=False), |
| #400 | AccountMeta(bonding_curve, is_signer=False, is_writable=True), |
| #401 | AccountMeta(associated_bonding_curve, is_signer=False, is_writable=True), |
| #402 | AccountMeta(PUMP_GLOBAL, is_signer=False, is_writable=False), |
| #403 | AccountMeta(METAPLEX_PROGRAM_ID, is_signer=False, is_writable=False), |
| #404 | AccountMeta(metadata_pda, is_signer=False, is_writable=True), |
| #405 | AccountMeta(self.wallet_pubkey, is_signer=True, is_writable=True), |
| #406 | AccountMeta(SYSTEM_PROGRAM_ID, is_signer=False, is_writable=False), |
| #407 | AccountMeta(TOKEN_PROGRAM_ID, is_signer=False, is_writable=False), |
| #408 | AccountMeta(ASSOCIATED_TOKEN_PROGRAM_ID, is_signer=False, is_writable=False), |
| #409 | AccountMeta(SYSVAR_RENT_ID, is_signer=False, is_writable=False), |
| #410 | AccountMeta(event_authority, is_signer=False, is_writable=False), |
| #411 | AccountMeta(PUMP_PROGRAM_ID, is_signer=False, is_writable=False), |
| #412 | ] |
| #413 | |
| #414 | create_ix = Instruction(PUMP_PROGRAM_ID, data, accounts) |
| #415 | |
| #416 | # Build transaction |
| #417 | blockhash = await self.get_latest_blockhash() |
| #418 | |
| #419 | instructions = [create_ix] |
| #420 | |
| #421 | # Add initial buy if requested |
| #422 | if initial_buy_sol > 0: |
| #423 | buy_ix = await self._build_buy_instruction( |
| #424 | mint=mint, |
| #425 | bonding_curve=bonding_curve, |
| #426 | associated_bonding_curve=associated_bonding_curve, |
| #427 | amount=int(initial_buy_sol * 1_000_000_000), # Convert to lamports |
| #428 | max_sol_cost=int(initial_buy_sol * 1.1 * 1_000_000_000), # 10% slippage |
| #429 | ) |
| #430 | instructions.append(buy_ix) |
| #431 | |
| #432 | message = MessageV0.try_compile( |
| #433 | payer=self.wallet_pubkey, |
| #434 | instructions=instructions, |
| #435 | address_lookup_table_accounts=[], |
| #436 | recent_blockhash=blockhash, |
| #437 | ) |
| #438 | |
| #439 | tx = VersionedTransaction(message, [self.keypair, mint_keypair]) |
| #440 | |
| #441 | # Send transaction |
| #442 | signature = await self.send_transaction(tx) |
| #443 | |
| #444 | return CreateTokenResult( |
| #445 | mint=mint, |
| #446 | bonding_curve=bonding_curve, |
| #447 | signature=signature, |
| #448 | token_url=f"https://pump.fun/{mint}", |
| #449 | ) |
| #450 | |
| #451 | # =================== |
| #452 | # Trading |
| #453 | # =================== |
| #454 | |
| #455 | async def _build_buy_instruction( |
| #456 | self, |
| #457 | mint: Pubkey, |
| #458 | bonding_curve: Pubkey, |
| #459 | associated_bonding_curve: Pubkey, |
| #460 | amount: int, |
| #461 | max_sol_cost: int, |
| #462 | ) -> Instruction: |
| #463 | """Build buy instruction""" |
| #464 | user_token_account = self.get_associated_token_address(self.wallet_pubkey, mint) |
| #465 | |
| #466 | # Serialize data: discriminator + amount (u64) + max_sol_cost (u64) |
| #467 | data = BUY_DISCRIMINATOR |
| #468 | data += struct.pack("<Q", amount) |
| #469 | data += struct.pack("<Q", max_sol_cost) |
| #470 | |
| #471 | accounts = [ |
| #472 | AccountMeta(PUMP_GLOBAL, is_signer=False, is_writable=False), |
| #473 | AccountMeta(PUMP_FEE_RECIPIENT, is_signer=False, is_writable=True), |
| #474 | AccountMeta(mint, is_signer=False, is_writable=False), |
| #475 | AccountMeta(bonding_curve, is_signer=False, is_writable=True), |
| #476 | AccountMeta(associated_bonding_curve, is_signer=False, is_writable=True), |
| #477 | AccountMeta(user_token_account, is_signer=False, is_writable=True), |
| #478 | AccountMeta(self.wallet_pubkey, is_signer=True, is_writable=True), |
| #479 | AccountMeta(SYSTEM_PROGRAM_ID, is_signer=False, is_writable=False), |
| #480 | AccountMeta(TOKEN_PROGRAM_ID, is_signer=False, is_writable=False), |
| #481 | AccountMeta(SYSVAR_RENT_ID, is_signer=False, is_writable=False), |
| #482 | AccountMeta(self.get_event_authority_pda(), is_signer=False, is_writable=False), |
| #483 | AccountMeta(PUMP_PROGRAM_ID, is_signer=False, is_writable=False), |
| #484 | ] |
| #485 | |
| #486 | return Instruction(PUMP_PROGRAM_ID, data, accounts) |
| #487 | |
| #488 | async def buy( |
| #489 | self, |
| #490 | mint: Pubkey, |
| #491 | sol_amount: float, |
| #492 | slippage_bps: int = 500, |
| #493 | ) -> str: |
| #494 | """ |
| #495 | Buy tokens from a pump.fun bonding curve. |
| #496 | |
| #497 | Args: |
| #498 | mint: Token mint address |
| #499 | sol_amount: Amount of SOL to spend |
| #500 | slippage_bps: Slippage tolerance in basis points (default 5%) |
| #501 | |
| #502 | Returns: |
| #503 | Transaction signature |
| #504 | """ |
| #505 | if not self.keypair: |
| #506 | raise ValueError("Keypair required for trading") |
| #507 | |
| #508 | # Get bonding curve state to calculate expected tokens |
| #509 | state = await self.get_bonding_curve_state(mint) |
| #510 | if not state: |
| #511 | raise ValueError(f"Bonding curve not found for {mint}") |
| #512 | |
| #513 | if state.complete: |
| #514 | raise ValueError("Bonding curve is complete - trade on PumpSwap instead") |
| #515 | |
| #516 | # Calculate expected tokens using constant product formula |
| #517 | lamports = int(sol_amount * 1_000_000_000) |
| #518 | |
| #519 | # k = virtual_sol * virtual_token |
| #520 | # new_virtual_sol = virtual_sol + lamports |
| #521 | # new_virtual_token = k / new_virtual_sol |
| #522 | # tokens_out = virtual_token - new_virtual_token |
| #523 | k = state.virtual_sol_reserves * state.virtual_token_reserves |
| #524 | new_virtual_sol = state.virtual_sol_reserves + lamports |
| #525 | new_virtual_token = k // new_virtual_sol |
| #526 | tokens_out = state.virtual_token_reserves - new_virtual_token |
| #527 | |
| #528 | # Apply slippage |
| #529 | max_sol_cost = int(lamports * (1 + slippage_bps / 10000)) |
| #530 | |
| #531 | # Build and send transaction |
| #532 | bonding_curve = self.get_bonding_curve_pda(mint) |
| #533 | associated_bonding_curve = self.get_associated_token_address(bonding_curve, mint) |
| #534 | |
| #535 | buy_ix = await self._build_buy_instruction( |
| #536 | mint=mint, |
| #537 | bonding_curve=bonding_curve, |
| #538 | associated_bonding_curve=associated_bonding_curve, |
| #539 | amount=tokens_out, |
| #540 | max_sol_cost=max_sol_cost, |
| #541 | ) |
| #542 | |
| #543 | blockhash = await self.get_latest_blockhash() |
| #544 | message = MessageV0.try_compile( |
| #545 | payer=self.wallet_pubkey, |
| #546 | instructions=[buy_ix], |
| #547 | address_lookup_table_accounts=[], |
| #548 | recent_blockhash=blockhash, |
| #549 | ) |
| #550 | |
| #551 | tx = VersionedTransaction(message, [self.keypair]) |
| #552 | return await self.send_transaction(tx) |
| #553 | |
| #554 | async def sell( |
| #555 | self, |
| #556 | mint: Pubkey, |
| #557 | token_amount: int, |
| #558 | slippage_bps: int = 500, |
| #559 | ) -> str: |
| #560 | """ |
| #561 | Sell tokens to a pump.fun bonding curve. |
| #562 | |
| #563 | Args: |
| #564 | mint: Token mint address |
| #565 | token_amount: Amount of tokens to sell (in smallest unit) |
| #566 | slippage_bps: Slippage tolerance in basis points (default 5%) |
| #567 | |
| #568 | Returns: |
| #569 | Transaction signature |
| #570 | """ |
| #571 | if not self.keypair: |
| #572 | raise ValueError("Keypair required for trading") |
| #573 | |
| #574 | # Get bonding curve state |
| #575 | state = await self.get_bonding_curve_state(mint) |
| #576 | if not state: |
| #577 | raise ValueError(f"Bonding curve not found for {mint}") |
| #578 | |
| #579 | if state.complete: |
| #580 | raise ValueError("Bonding curve is complete - trade on PumpSwap instead") |
| #581 | |
| #582 | # Calculate expected SOL using constant product formula |
| #583 | # k = virtual_sol * virtual_token |
| #584 | # new_virtual_token = virtual_token + token_amount |
| #585 | # new_virtual_sol = k / new_virtual_token |
| #586 | # sol_out = virtual_sol - new_virtual_sol |
| #587 | k = state.virtual_sol_reserves * state.virtual_token_reserves |
| #588 | new_virtual_token = state.virtual_token_reserves + token_amount |
| #589 | new_virtual_sol = k // new_virtual_token |
| #590 | sol_out = state.virtual_sol_reserves - new_virtual_sol |
| #591 | |
| #592 | # Apply slippage |
| #593 | min_sol_output = int(sol_out * (1 - slippage_bps / 10000)) |
| #594 | |
| #595 | # Build accounts |
| #596 | bonding_curve = self.get_bonding_curve_pda(mint) |
| #597 | associated_bonding_curve = self.get_associated_token_address(bonding_curve, mint) |
| #598 | user_token_account = self.get_associated_token_address(self.wallet_pubkey, mint) |
| #599 | |
| #600 | # Serialize data |
| #601 | data = SELL_DISCRIMINATOR |
| #602 | data += struct.pack("<Q", token_amount) |
| #603 | data += struct.pack("<Q", min_sol_output) |
| #604 | |
| #605 | accounts = [ |
| #606 | AccountMeta(PUMP_GLOBAL, is_signer=False, is_writable=False), |
| #607 | AccountMeta(PUMP_FEE_RECIPIENT, is_signer=False, is_writable=True), |
| #608 | AccountMeta(mint, is_signer=False, is_writable=False), |
| #609 | AccountMeta(bonding_curve, is_signer=False, is_writable=True), |
| #610 | AccountMeta(associated_bonding_curve, is_signer=False, is_writable=True), |
| #611 | AccountMeta(user_token_account, is_signer=False, is_writable=True), |
| #612 | AccountMeta(self.wallet_pubkey, is_signer=True, is_writable=True), |
| #613 | AccountMeta(SYSTEM_PROGRAM_ID, is_signer=False, is_writable=False), |
| #614 | AccountMeta(ASSOCIATED_TOKEN_PROGRAM_ID, is_signer=False, is_writable=False), |
| #615 | AccountMeta(TOKEN_PROGRAM_ID, is_signer=False, is_writable=False), |
| #616 | AccountMeta(self.get_event_authority_pda(), is_signer=False, is_writable=False), |
| #617 | AccountMeta(PUMP_PROGRAM_ID, is_signer=False, is_writable=False), |
| #618 | ] |
| #619 | |
| #620 | sell_ix = Instruction(PUMP_PROGRAM_ID, data, accounts) |
| #621 | |
| #622 | blockhash = await self.get_latest_blockhash() |
| #623 | message = MessageV0.try_compile( |
| #624 | payer=self.wallet_pubkey, |
| #625 | instructions=[sell_ix], |
| #626 | address_lookup_table_accounts=[], |
| #627 | recent_blockhash=blockhash, |
| #628 | ) |
| #629 | |
| #630 | tx = VersionedTransaction(message, [self.keypair]) |
| #631 | return await self.send_transaction(tx) |
| #632 | |
| #633 | # =================== |
| #634 | # Utility Methods |
| #635 | # =================== |
| #636 | |
| #637 | async def get_token_price(self, mint: Pubkey) -> Optional[dict]: |
| #638 | """Get current token price from bonding curve""" |
| #639 | state = await self.get_bonding_curve_state(mint) |
| #640 | if not state: |
| #641 | return None |
| #642 | |
| #643 | # Price = virtual_sol_reserves / virtual_token_reserves |
| #644 | price_per_token = state.virtual_sol_reserves / state.virtual_token_reserves |
| #645 | price_per_token_sol = price_per_token / 1_000_000_000 |
| #646 | |
| #647 | # Market cap = price * total_supply |
| #648 | market_cap_lamports = price_per_token * state.token_total_supply |
| #649 | market_cap_sol = market_cap_lamports / 1_000_000_000 |
| #650 | |
| #651 | return { |
| #652 | "price_per_token_lamports": price_per_token, |
| #653 | "price_per_token_sol": price_per_token_sol, |
| #654 | "market_cap_lamports": market_cap_lamports, |
| #655 | "market_cap_sol": market_cap_sol, |
| #656 | "virtual_sol_reserves": state.virtual_sol_reserves / 1_000_000_000, |
| #657 | "virtual_token_reserves": state.virtual_token_reserves, |
| #658 | "real_sol_reserves": state.real_sol_reserves / 1_000_000_000, |
| #659 | "real_token_reserves": state.real_token_reserves, |
| #660 | "complete": state.complete, |
| #661 | "progress_percent": (1 - state.real_token_reserves / 793_100_000_000_000) * 100, |
| #662 | } |
| #663 | |
| #664 | async def close(self): |
| #665 | """Close HTTP clients""" |
| #666 | await self._http_client.aclose() |
| #667 | await self._rpc_client.aclose() |
| #668 | |
| #669 | async def __aenter__(self): |
| #670 | return self |
| #671 | |
| #672 | async def __aexit__(self, exc_type, exc_val, exc_tb): |
| #673 | await self.close() |
| #674 |