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 | """Bags Trading Client - Python wrapper for Bags SDK API""" |
| #2 | |
| #3 | import httpx |
| #4 | import base58 |
| #5 | import os |
| #6 | from pathlib import Path |
| #7 | from typing import Optional, Any, BinaryIO |
| #8 | from dataclasses import dataclass |
| #9 | from solders.keypair import Keypair |
| #10 | from solders.pubkey import Pubkey |
| #11 | from solders.transaction import VersionedTransaction |
| #12 | from solders.message import to_bytes_versioned |
| #13 | |
| #14 | |
| #15 | BAGS_API_BASE_URL = "https://public-api-v2.bags.fm/api/v1" |
| #16 | WRAPPED_SOL_MINT = "So11111111111111111111111111111111111111112" |
| #17 | GLOBAL_LUT = "Eq1EVs15EAWww1YtPTtWPzJRLPJoS6VYP9oW9SbNr3yp" |
| #18 | |
| #19 | |
| #20 | @dataclass |
| #21 | class TradeQuote: |
| #22 | """Trade quote response from Bags API""" |
| #23 | context_slot: int |
| #24 | in_amount: str |
| #25 | input_mint: str |
| #26 | min_out_amount: str |
| #27 | out_amount: str |
| #28 | output_mint: str |
| #29 | price_impact_pct: str |
| #30 | slippage_bps: int |
| #31 | request_id: str |
| #32 | route_plan: list |
| #33 | raw_response: dict |
| #34 | |
| #35 | |
| #36 | @dataclass |
| #37 | class SwapResult: |
| #38 | """Swap transaction result""" |
| #39 | transaction: VersionedTransaction |
| #40 | compute_unit_limit: int |
| #41 | last_valid_block_height: int |
| #42 | prioritization_fee_lamports: int |
| #43 | |
| #44 | |
| #45 | class BagsClient: |
| #46 | """Python client for Bags Trading API""" |
| #47 | |
| #48 | def __init__( |
| #49 | self, |
| #50 | api_key: str, |
| #51 | config_key: str, |
| #52 | rpc_url: str, |
| #53 | private_key: Optional[str] = None, |
| #54 | base_url: str = BAGS_API_BASE_URL, |
| #55 | ): |
| #56 | """ |
| #57 | Initialize Bags trading client. |
| #58 | |
| #59 | Args: |
| #60 | api_key: Bags API key |
| #61 | config_key: Bags config key |
| #62 | rpc_url: Solana RPC URL (Helius) |
| #63 | private_key: Wallet private key (base58 encoded) for signing transactions |
| #64 | base_url: Bags API base URL |
| #65 | """ |
| #66 | self.api_key = api_key |
| #67 | self.config_key = config_key |
| #68 | self.rpc_url = rpc_url |
| #69 | self.base_url = base_url |
| #70 | self.keypair = None |
| #71 | |
| #72 | if private_key: |
| #73 | try: |
| #74 | # Decode base58 private key |
| #75 | secret_bytes = base58.b58decode(private_key) |
| #76 | self.keypair = Keypair.from_bytes(secret_bytes) |
| #77 | except Exception as e: |
| #78 | raise ValueError(f"Invalid private key: {e}") |
| #79 | |
| #80 | self._client = httpx.AsyncClient( |
| #81 | base_url=self.base_url, |
| #82 | headers={ |
| #83 | "x-api-key": api_key, |
| #84 | "Content-Type": "application/json", |
| #85 | }, |
| #86 | timeout=60.0 |
| #87 | ) |
| #88 | |
| #89 | @property |
| #90 | def wallet_pubkey(self) -> Optional[str]: |
| #91 | """Get wallet public key if keypair is set""" |
| #92 | if self.keypair: |
| #93 | return str(self.keypair.pubkey()) |
| #94 | return None |
| #95 | |
| #96 | async def get_quote( |
| #97 | self, |
| #98 | input_mint: str, |
| #99 | output_mint: str, |
| #100 | amount: int, |
| #101 | slippage_bps: int = 300, # 3% default slippage |
| #102 | slippage_mode: str = "manual" |
| #103 | ) -> TradeQuote: |
| #104 | """ |
| #105 | Get a swap quote for a token trade. |
| #106 | |
| #107 | Args: |
| #108 | input_mint: Input token mint address |
| #109 | output_mint: Output token mint address |
| #110 | amount: Amount in smallest unit (lamports for SOL) |
| #111 | slippage_bps: Slippage tolerance in basis points |
| #112 | slippage_mode: 'manual' or 'auto' |
| #113 | |
| #114 | Returns: |
| #115 | TradeQuote with route and pricing info |
| #116 | """ |
| #117 | params = { |
| #118 | "inputMint": input_mint, |
| #119 | "outputMint": output_mint, |
| #120 | "amount": amount, |
| #121 | "slippageMode": slippage_mode, |
| #122 | "slippageBps": slippage_bps, |
| #123 | } |
| #124 | |
| #125 | response = await self._client.get("/trade/quote", params=params) |
| #126 | response.raise_for_status() |
| #127 | data = response.json() |
| #128 | |
| #129 | if not data.get("success"): |
| #130 | raise Exception(f"Quote failed: {data.get('error', 'Unknown error')}") |
| #131 | |
| #132 | result = data["response"] |
| #133 | |
| #134 | return TradeQuote( |
| #135 | context_slot=result.get("contextSlot", 0), |
| #136 | in_amount=result["inAmount"], |
| #137 | input_mint=result["inputMint"], |
| #138 | min_out_amount=result["minOutAmount"], |
| #139 | out_amount=result["outAmount"], |
| #140 | output_mint=result["outputMint"], |
| #141 | price_impact_pct=result.get("priceImpactPct", "0"), |
| #142 | slippage_bps=result["slippageBps"], |
| #143 | request_id=result["requestId"], |
| #144 | route_plan=result.get("routePlan", []), |
| #145 | raw_response=result, |
| #146 | ) |
| #147 | |
| #148 | async def create_swap_transaction( |
| #149 | self, |
| #150 | quote: TradeQuote, |
| #151 | user_pubkey: Optional[str] = None, |
| #152 | ) -> SwapResult: |
| #153 | """ |
| #154 | Create a swap transaction from a quote. |
| #155 | |
| #156 | Args: |
| #157 | quote: TradeQuote from get_quote() |
| #158 | user_pubkey: User public key (uses wallet_pubkey if not provided) |
| #159 | |
| #160 | Returns: |
| #161 | SwapResult with versioned transaction ready for signing |
| #162 | """ |
| #163 | if user_pubkey is None: |
| #164 | if self.wallet_pubkey is None: |
| #165 | raise ValueError("No wallet configured and no user_pubkey provided") |
| #166 | user_pubkey = self.wallet_pubkey |
| #167 | |
| #168 | payload = { |
| #169 | "quoteResponse": quote.raw_response, |
| #170 | "userPublicKey": user_pubkey, |
| #171 | } |
| #172 | |
| #173 | response = await self._client.post("/trade/swap", json=payload) |
| #174 | response.raise_for_status() |
| #175 | data = response.json() |
| #176 | |
| #177 | if not data.get("success"): |
| #178 | raise Exception(f"Swap transaction creation failed: {data.get('error', 'Unknown error')}") |
| #179 | |
| #180 | result = data["response"] |
| #181 | |
| #182 | # Decode the transaction |
| #183 | tx_bytes = base58.b58decode(result["swapTransaction"]) |
| #184 | transaction = VersionedTransaction.from_bytes(tx_bytes) |
| #185 | |
| #186 | return SwapResult( |
| #187 | transaction=transaction, |
| #188 | compute_unit_limit=result["computeUnitLimit"], |
| #189 | last_valid_block_height=result["lastValidBlockHeight"], |
| #190 | prioritization_fee_lamports=result["prioritizationFeeLamports"], |
| #191 | ) |
| #192 | |
| #193 | async def sign_and_send_transaction( |
| #194 | self, |
| #195 | swap_result: SwapResult, |
| #196 | ) -> str: |
| #197 | """ |
| #198 | Sign and send a swap transaction. |
| #199 | |
| #200 | Args: |
| #201 | swap_result: SwapResult from create_swap_transaction() |
| #202 | |
| #203 | Returns: |
| #204 | Transaction signature |
| #205 | """ |
| #206 | if self.keypair is None: |
| #207 | raise ValueError("No keypair configured for signing") |
| #208 | |
| #209 | # Sign the versioned transaction correctly |
| #210 | tx = swap_result.transaction |
| #211 | |
| #212 | # Get message bytes and sign |
| #213 | message_bytes = to_bytes_versioned(tx.message) |
| #214 | signature = self.keypair.sign_message(message_bytes) |
| #215 | |
| #216 | # Create signed transaction |
| #217 | signed_tx = VersionedTransaction.populate(tx.message, [signature]) |
| #218 | |
| #219 | # Serialize and encode |
| #220 | tx_bytes = bytes(signed_tx) |
| #221 | tx_base58 = base58.b58encode(tx_bytes).decode() |
| #222 | |
| #223 | # Send via Jito bundle or direct RPC |
| #224 | tx_signature = await self._send_bundle([tx_base58]) |
| #225 | |
| #226 | return tx_signature |
| #227 | |
| #228 | async def _send_bundle(self, transactions: list[str], region: str = "mainnet") -> str: |
| #229 | """ |
| #230 | Send transactions via Bags Jito bundle endpoint. |
| #231 | |
| #232 | Args: |
| #233 | transactions: List of base58 encoded serialized transactions |
| #234 | region: Jito region |
| #235 | |
| #236 | Returns: |
| #237 | Bundle ID |
| #238 | """ |
| #239 | payload = { |
| #240 | "transactions": transactions, |
| #241 | "region": region, |
| #242 | } |
| #243 | |
| #244 | response = await self._client.post("/solana/send-bundle", json=payload) |
| #245 | response.raise_for_status() |
| #246 | data = response.json() |
| #247 | |
| #248 | if not data.get("success"): |
| #249 | raise Exception(f"Bundle send failed: {data.get('error', 'Unknown error')}") |
| #250 | |
| #251 | return data["response"] |
| #252 | |
| #253 | async def get_bundle_status(self, bundle_ids: list[str], region: str = "mainnet") -> dict: |
| #254 | """ |
| #255 | Get status of submitted bundles. |
| #256 | |
| #257 | Args: |
| #258 | bundle_ids: List of bundle IDs to check |
| #259 | region: Jito region |
| #260 | |
| #261 | Returns: |
| #262 | Bundle status response |
| #263 | """ |
| #264 | payload = { |
| #265 | "bundleIds": bundle_ids, |
| #266 | "region": region, |
| #267 | } |
| #268 | |
| #269 | response = await self._client.post("/solana/get-bundle-statuses", json=payload) |
| #270 | response.raise_for_status() |
| #271 | data = response.json() |
| #272 | |
| #273 | if not data.get("success"): |
| #274 | raise Exception(f"Bundle status check failed: {data.get('error', 'Unknown error')}") |
| #275 | |
| #276 | return data["response"] |
| #277 | |
| #278 | async def get_jito_fees(self) -> dict: |
| #279 | """ |
| #280 | Get current Jito tip fee percentiles. |
| #281 | |
| #282 | Returns: |
| #283 | Jito fee info |
| #284 | """ |
| #285 | response = await self._client.get("/solana/jito-recent-fees") |
| #286 | response.raise_for_status() |
| #287 | data = response.json() |
| #288 | |
| #289 | if not data.get("success"): |
| #290 | raise Exception(f"Jito fees fetch failed: {data.get('error', 'Unknown error')}") |
| #291 | |
| #292 | return data["response"] |
| #293 | |
| #294 | async def execute_swap( |
| #295 | self, |
| #296 | input_mint: str, |
| #297 | output_mint: str, |
| #298 | amount: int, |
| #299 | slippage_bps: int = 300, |
| #300 | ) -> dict: |
| #301 | """ |
| #302 | High-level method to execute a complete swap. |
| #303 | |
| #304 | Args: |
| #305 | input_mint: Input token mint |
| #306 | output_mint: Output token mint |
| #307 | amount: Amount to swap |
| #308 | slippage_bps: Slippage tolerance |
| #309 | |
| #310 | Returns: |
| #311 | Dict with quote info and transaction signature |
| #312 | """ |
| #313 | # Get quote |
| #314 | quote = await self.get_quote( |
| #315 | input_mint=input_mint, |
| #316 | output_mint=output_mint, |
| #317 | amount=amount, |
| #318 | slippage_bps=slippage_bps, |
| #319 | ) |
| #320 | |
| #321 | # Create swap transaction |
| #322 | swap_result = await self.create_swap_transaction(quote) |
| #323 | |
| #324 | # Sign and send |
| #325 | signature = await self.sign_and_send_transaction(swap_result) |
| #326 | |
| #327 | return { |
| #328 | "signature": signature, |
| #329 | "quote": { |
| #330 | "input_mint": quote.input_mint, |
| #331 | "output_mint": quote.output_mint, |
| #332 | "in_amount": quote.in_amount, |
| #333 | "out_amount": quote.out_amount, |
| #334 | "min_out_amount": quote.min_out_amount, |
| #335 | "price_impact_pct": quote.price_impact_pct, |
| #336 | "slippage_bps": quote.slippage_bps, |
| #337 | } |
| #338 | } |
| #339 | |
| #340 | async def buy_token( |
| #341 | self, |
| #342 | token_mint: str, |
| #343 | sol_amount: float, |
| #344 | slippage_bps: int = 300, |
| #345 | ) -> dict: |
| #346 | """ |
| #347 | Buy a token with SOL. |
| #348 | |
| #349 | Args: |
| #350 | token_mint: Token to buy |
| #351 | sol_amount: Amount of SOL to spend |
| #352 | slippage_bps: Slippage tolerance |
| #353 | |
| #354 | Returns: |
| #355 | Swap result |
| #356 | """ |
| #357 | lamports = int(sol_amount * 1_000_000_000) # Convert SOL to lamports |
| #358 | |
| #359 | return await self.execute_swap( |
| #360 | input_mint=WRAPPED_SOL_MINT, |
| #361 | output_mint=token_mint, |
| #362 | amount=lamports, |
| #363 | slippage_bps=slippage_bps, |
| #364 | ) |
| #365 | |
| #366 | async def sell_token( |
| #367 | self, |
| #368 | token_mint: str, |
| #369 | token_amount: int, |
| #370 | slippage_bps: int = 300, |
| #371 | ) -> dict: |
| #372 | """ |
| #373 | Sell a token for SOL. |
| #374 | |
| #375 | Args: |
| #376 | token_mint: Token to sell |
| #377 | token_amount: Amount of tokens to sell (in smallest unit) |
| #378 | slippage_bps: Slippage tolerance |
| #379 | |
| #380 | Returns: |
| #381 | Swap result |
| #382 | """ |
| #383 | return await self.execute_swap( |
| #384 | input_mint=token_mint, |
| #385 | output_mint=WRAPPED_SOL_MINT, |
| #386 | amount=token_amount, |
| #387 | slippage_bps=slippage_bps, |
| #388 | ) |
| #389 | |
| #390 | # ===================== |
| #391 | # Token Launch V2 Methods |
| #392 | # ===================== |
| #393 | |
| #394 | async def create_token_metadata( |
| #395 | self, |
| #396 | name: str, |
| #397 | symbol: str, |
| #398 | description: str, |
| #399 | image_url: Optional[str] = None, |
| #400 | image_file_path: Optional[str] = None, |
| #401 | twitter: Optional[str] = None, |
| #402 | website: Optional[str] = None, |
| #403 | telegram: Optional[str] = None, |
| #404 | ) -> dict: |
| #405 | """ |
| #406 | Create token info and metadata for launching. |
| #407 | |
| #408 | Args: |
| #409 | name: Token name |
| #410 | symbol: Token symbol |
| #411 | description: Token description |
| #412 | image_url: URL to token image (mutually exclusive with image_file_path) |
| #413 | image_file_path: Path to local image file to upload (mutually exclusive with image_url) |
| #414 | Supports PNG, JPG, JPEG, GIF, WebP (max 15MB) |
| #415 | twitter: Twitter URL |
| #416 | website: Website URL |
| #417 | telegram: Telegram URL |
| #418 | |
| #419 | Returns: |
| #420 | Dict with tokenMint and tokenMetadata (IPFS URI) |
| #421 | """ |
| #422 | # Validate that only one image source is provided |
| #423 | if image_url and image_file_path: |
| #424 | raise ValueError("Cannot specify both image_url and image_file_path. Use one or the other.") |
| #425 | |
| #426 | # Handle file upload |
| #427 | if image_file_path: |
| #428 | return await self._create_token_metadata_with_file( |
| #429 | name=name, |
| #430 | symbol=symbol, |
| #431 | description=description, |
| #432 | image_file_path=image_file_path, |
| #433 | twitter=twitter, |
| #434 | website=website, |
| #435 | telegram=telegram, |
| #436 | ) |
| #437 | |
| #438 | # Handle URL-based metadata (original behavior) |
| #439 | payload = { |
| #440 | "name": name, |
| #441 | "symbol": symbol.upper().replace("$", ""), |
| #442 | "description": description, |
| #443 | } |
| #444 | |
| #445 | if image_url: |
| #446 | payload["imageUrl"] = image_url |
| #447 | if twitter: |
| #448 | payload["twitter"] = twitter |
| #449 | if website: |
| #450 | payload["website"] = website |
| #451 | if telegram: |
| #452 | payload["telegram"] = telegram |
| #453 | |
| #454 | response = await self._client.post("/token-launch/create-token-info", json=payload) |
| #455 | response.raise_for_status() |
| #456 | data = response.json() |
| #457 | |
| #458 | if not data.get("success"): |
| #459 | raise Exception(f"Token info creation failed: {data.get('error', 'Unknown error')}") |
| #460 | |
| #461 | return data["response"] |
| #462 | |
| #463 | async def _create_token_metadata_with_file( |
| #464 | self, |
| #465 | name: str, |
| #466 | symbol: str, |
| #467 | description: str, |
| #468 | image_file_path: str, |
| #469 | twitter: Optional[str] = None, |
| #470 | website: Optional[str] = None, |
| #471 | telegram: Optional[str] = None, |
| #472 | ) -> dict: |
| #473 | """ |
| #474 | Create token metadata with file upload. |
| #475 | |
| #476 | Args: |
| #477 | name: Token name |
| #478 | symbol: Token symbol |
| #479 | description: Token description |
| #480 | image_file_path: Path to local image file |
| #481 | twitter: Twitter URL |
| #482 | website: Website URL |
| #483 | telegram: Telegram URL |
| #484 | |
| #485 | Returns: |
| #486 | Dict with tokenMint and tokenMetadata (IPFS URI) |
| #487 | """ |
| #488 | # Validate file exists |
| #489 | file_path = Path(image_file_path) |
| #490 | if not file_path.exists(): |
| #491 | raise FileNotFoundError(f"Image file not found: {image_file_path}") |
| #492 | |
| #493 | # Validate file size (15MB max) |
| #494 | file_size = file_path.stat().st_size |
| #495 | max_size = 15 * 1024 * 1024 # 15MB in bytes |
| #496 | if file_size > max_size: |
| #497 | raise ValueError(f"Image file too large: {file_size / 1024 / 1024:.2f}MB (max 15MB)") |
| #498 | |
| #499 | # Validate file type |
| #500 | allowed_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.webp'} |
| #501 | file_ext = file_path.suffix.lower() |
| #502 | if file_ext not in allowed_extensions: |
| #503 | raise ValueError(f"Unsupported file type: {file_ext}. Allowed: {', '.join(allowed_extensions)}") |
| #504 | |
| #505 | # Determine MIME type |
| #506 | mime_types = { |
| #507 | '.png': 'image/png', |
| #508 | '.jpg': 'image/jpeg', |
| #509 | '.jpeg': 'image/jpeg', |
| #510 | '.gif': 'image/gif', |
| #511 | '.webp': 'image/webp', |
| #512 | } |
| #513 | mime_type = mime_types.get(file_ext, 'application/octet-stream') |
| #514 | |
| #515 | # Build multipart form data |
| #516 | files = { |
| #517 | 'image': (file_path.name, open(file_path, 'rb'), mime_type), |
| #518 | } |
| #519 | |
| #520 | data = { |
| #521 | 'name': name, |
| #522 | 'symbol': symbol.upper().replace("$", ""), |
| #523 | 'description': description, |
| #524 | } |
| #525 | |
| #526 | if twitter: |
| #527 | data['twitter'] = twitter |
| #528 | if website: |
| #529 | data['website'] = website |
| #530 | if telegram: |
| #531 | data['telegram'] = telegram |
| #532 | |
| #533 | # Create a temporary client without Content-Type header (httpx will set it for multipart) |
| #534 | async with httpx.AsyncClient( |
| #535 | base_url=self.base_url, |
| #536 | headers={ |
| #537 | "x-api-key": self.api_key, |
| #538 | # No Content-Type header - httpx will add multipart/form-data with boundary |
| #539 | }, |
| #540 | timeout=120.0 # Longer timeout for file uploads |
| #541 | ) as upload_client: |
| #542 | try: |
| #543 | response = await upload_client.post( |
| #544 | "/token-launch/create-token-info", |
| #545 | data=data, |
| #546 | files=files, |
| #547 | ) |
| #548 | response.raise_for_status() |
| #549 | result = response.json() |
| #550 | |
| #551 | if not result.get("success"): |
| #552 | raise Exception(f"Token info creation failed: {result.get('error', 'Unknown error')}") |
| #553 | |
| #554 | return result["response"] |
| #555 | finally: |
| #556 | # Close the file handle |
| #557 | files['image'][1].close() |
| #558 | |
| #559 | async def create_fee_share_config( |
| #560 | self, |
| #561 | token_mint: str, |
| #562 | fee_claimers: list[dict], |
| #563 | partner_wallet: Optional[str] = None, |
| #564 | ) -> dict: |
| #565 | """ |
| #566 | Create fee share configuration for a token. |
| #567 | |
| #568 | Args: |
| #569 | token_mint: Token mint address |
| #570 | fee_claimers: List of {wallet: str, bps: int} (must sum to 10000) |
| #571 | partner_wallet: Optional partner wallet |
| #572 | |
| #573 | Returns: |
| #574 | Dict with configKey and transactions |
| #575 | """ |
| #576 | # Validate BPS total |
| #577 | total_bps = sum(fc.get("bps", 0) for fc in fee_claimers) |
| #578 | if total_bps != 10000: |
| #579 | raise ValueError(f"Fee claimer BPS must total 10000, got {total_bps}") |
| #580 | |
| #581 | payload = { |
| #582 | "baseMint": token_mint, |
| #583 | "payer": self.wallet_pubkey, |
| #584 | "feeClaimers": [ |
| #585 | {"user": fc["wallet"], "userBps": fc["bps"]} |
| #586 | for fc in fee_claimers |
| #587 | ], |
| #588 | } |
| #589 | |
| #590 | if partner_wallet: |
| #591 | payload["partner"] = partner_wallet |
| #592 | |
| #593 | response = await self._client.post("/fee-share/config", json=payload) |
| #594 | response.raise_for_status() |
| #595 | data = response.json() |
| #596 | |
| #597 | if not data.get("success"): |
| #598 | raise Exception(f"Fee share config creation failed: {data.get('error', 'Unknown error')}") |
| #599 | |
| #600 | return data["response"] |
| #601 | |
| #602 | async def create_launch_transaction( |
| #603 | self, |
| #604 | metadata_url: str, |
| #605 | token_mint: str, |
| #606 | config_key: str, |
| #607 | initial_buy_sol: float = 0.01, |
| #608 | tip_lamports: Optional[int] = None, |
| #609 | ) -> dict: |
| #610 | """ |
| #611 | Create a token launch transaction. |
| #612 | |
| #613 | Args: |
| #614 | metadata_url: IPFS metadata URI from create_token_metadata |
| #615 | token_mint: Token mint address |
| #616 | config_key: Config key from create_fee_share_config |
| #617 | initial_buy_sol: Initial buy amount in SOL |
| #618 | tip_lamports: Jito tip in lamports |
| #619 | |
| #620 | Returns: |
| #621 | Dict with transaction and blockhash info |
| #622 | """ |
| #623 | payload = { |
| #624 | "metadataUrl": metadata_url, |
| #625 | "tokenMint": token_mint, |
| #626 | "launchWallet": self.wallet_pubkey, |
| #627 | "initialBuyLamports": int(initial_buy_sol * 1_000_000_000), |
| #628 | "configKey": config_key, |
| #629 | } |
| #630 | |
| #631 | if tip_lamports: |
| #632 | payload["tipConfig"] = { |
| #633 | "tipLamports": tip_lamports, |
| #634 | } |
| #635 | |
| #636 | response = await self._client.post("/token-launch/create-launch-transaction", json=payload) |
| #637 | response.raise_for_status() |
| #638 | data = response.json() |
| #639 | |
| #640 | if not data.get("success"): |
| #641 | raise Exception(f"Launch transaction creation failed: {data.get('error', 'Unknown error')}") |
| #642 | |
| #643 | return data["response"] |
| #644 | |
| #645 | async def launch_token( |
| #646 | self, |
| #647 | name: str, |
| #648 | symbol: str, |
| #649 | description: str, |
| #650 | image_url: Optional[str] = None, |
| #651 | image_file_path: Optional[str] = None, |
| #652 | initial_buy_sol: float = 0.01, |
| #653 | twitter: Optional[str] = None, |
| #654 | website: Optional[str] = None, |
| #655 | telegram: Optional[str] = None, |
| #656 | fee_share_bps: int = 10000, # Creator gets 100% by default |
| #657 | ) -> dict: |
| #658 | """ |
| #659 | High-level method to launch a token (all steps). |
| #660 | |
| #661 | Args: |
| #662 | name: Token name |
| #663 | symbol: Token symbol |
| #664 | description: Token description |
| #665 | image_url: Token image URL (mutually exclusive with image_file_path) |
| #666 | image_file_path: Path to local image file (mutually exclusive with image_url) |
| #667 | initial_buy_sol: Initial buy in SOL |
| #668 | twitter: Twitter URL |
| #669 | website: Website URL |
| #670 | telegram: Telegram URL |
| #671 | fee_share_bps: Creator fee share (default 10000 = 100%) |
| #672 | |
| #673 | Returns: |
| #674 | Dict with tokenMint, signature, and tokenUrl |
| #675 | """ |
| #676 | if self.keypair is None: |
| #677 | raise ValueError("No keypair configured for launching") |
| #678 | |
| #679 | if not image_url and not image_file_path: |
| #680 | raise ValueError("Either image_url or image_file_path must be provided") |
| #681 | |
| #682 | # Step 1: Create token metadata |
| #683 | token_info = await self.create_token_metadata( |
| #684 | name=name, |
| #685 | symbol=symbol, |
| #686 | description=description, |
| #687 | image_url=image_url, |
| #688 | image_file_path=image_file_path, |
| #689 | twitter=twitter, |
| #690 | website=website, |
| #691 | telegram=telegram, |
| #692 | ) |
| #693 | |
| #694 | token_mint = token_info["tokenMint"] |
| #695 | metadata_url = token_info["tokenMetadata"] |
| #696 | |
| #697 | # Step 2: Create fee share config (creator gets all fees) |
| #698 | fee_claimers = [{"wallet": self.wallet_pubkey, "bps": fee_share_bps}] |
| #699 | config_result = await self.create_fee_share_config(token_mint, fee_claimers) |
| #700 | config_key = config_result["configKey"] |
| #701 | |
| #702 | # Sign and send config transactions if any |
| #703 | for tx_data in config_result.get("transactions", []): |
| #704 | tx_bytes = base58.b58decode(tx_data["transaction"]) |
| #705 | tx = VersionedTransaction.from_bytes(tx_bytes) |
| #706 | |
| #707 | # Sign the versioned transaction correctly |
| #708 | message_bytes = to_bytes_versioned(tx.message) |
| #709 | signature = self.keypair.sign_message(message_bytes) |
| #710 | signed_tx = VersionedTransaction.populate(tx.message, [signature]) |
| #711 | |
| #712 | tx_encoded = base58.b58encode(bytes(signed_tx)).decode() |
| #713 | await self._send_bundle([tx_encoded]) |
| #714 | |
| #715 | # Step 3: Create launch transaction |
| #716 | launch_result = await self.create_launch_transaction( |
| #717 | metadata_url=metadata_url, |
| #718 | token_mint=token_mint, |
| #719 | config_key=config_key, |
| #720 | initial_buy_sol=initial_buy_sol, |
| #721 | ) |
| #722 | |
| #723 | # Step 4: Sign and send launch transaction |
| #724 | tx_bytes = base58.b58decode(launch_result["transaction"]) |
| #725 | tx = VersionedTransaction.from_bytes(tx_bytes) |
| #726 | |
| #727 | # Sign the versioned transaction correctly |
| #728 | message_bytes = to_bytes_versioned(tx.message) |
| #729 | signature = self.keypair.sign_message(message_bytes) |
| #730 | signed_tx = VersionedTransaction.populate(tx.message, [signature]) |
| #731 | |
| #732 | tx_encoded = base58.b58encode(bytes(signed_tx)).decode() |
| #733 | tx_signature = await self._send_bundle([tx_encoded]) |
| #734 | |
| #735 | return { |
| #736 | "token_mint": token_mint, |
| #737 | "metadata_uri": metadata_url, |
| #738 | "config_key": config_key, |
| #739 | "signature": signature, |
| #740 | "token_url": f"https://bags.fm/{token_mint}", |
| #741 | } |
| #742 | |
| #743 | async def get_token_fees(self, token_mint: str) -> dict: |
| #744 | """ |
| #745 | Get lifetime fees for a token. |
| #746 | |
| #747 | Args: |
| #748 | token_mint: Token mint address |
| #749 | |
| #750 | Returns: |
| #751 | Fee info |
| #752 | """ |
| #753 | response = await self._client.get(f"/state/token/{token_mint}/fees") |
| #754 | response.raise_for_status() |
| #755 | data = response.json() |
| #756 | |
| #757 | if not data.get("success"): |
| #758 | raise Exception(f"Failed to get token fees: {data.get('error', 'Unknown error')}") |
| #759 | |
| #760 | return data["response"] |
| #761 | |
| #762 | async def get_claimable_fees(self) -> list[dict]: |
| #763 | """ |
| #764 | Get all claimable fee positions for the wallet. |
| #765 | |
| #766 | Returns: |
| #767 | List of claimable positions |
| #768 | """ |
| #769 | if self.wallet_pubkey is None: |
| #770 | raise ValueError("No wallet configured") |
| #771 | |
| #772 | response = await self._client.get( |
| #773 | "/fee/positions", |
| #774 | params={"wallet": self.wallet_pubkey} |
| #775 | ) |
| #776 | response.raise_for_status() |
| #777 | data = response.json() |
| #778 | |
| #779 | if not data.get("success"): |
| #780 | raise Exception(f"Failed to get claimable fees: {data.get('error', 'Unknown error')}") |
| #781 | |
| #782 | return data["response"] |
| #783 | |
| #784 | async def claim_fees(self, position: dict) -> str: |
| #785 | """ |
| #786 | Claim fees from a position. |
| #787 | |
| #788 | Args: |
| #789 | position: Position data from get_claimable_fees |
| #790 | |
| #791 | Returns: |
| #792 | Transaction signature |
| #793 | """ |
| #794 | if self.keypair is None: |
| #795 | raise ValueError("No keypair configured for claiming") |
| #796 | |
| #797 | response = await self._client.post( |
| #798 | "/fee/claim", |
| #799 | json={"position": position, "wallet": self.wallet_pubkey} |
| #800 | ) |
| #801 | response.raise_for_status() |
| #802 | data = response.json() |
| #803 | |
| #804 | if not data.get("success"): |
| #805 | raise Exception(f"Failed to create claim transaction: {data.get('error', 'Unknown error')}") |
| #806 | |
| #807 | # Sign and send |
| #808 | for tx_data in data["response"].get("transactions", []): |
| #809 | tx_bytes = base58.b58decode(tx_data["transaction"]) |
| #810 | tx = VersionedTransaction.from_bytes(tx_bytes) |
| #811 | |
| #812 | # Sign the versioned transaction correctly |
| #813 | message_bytes = to_bytes_versioned(tx.message) |
| #814 | signature = self.keypair.sign_message(message_bytes) |
| #815 | signed_tx = VersionedTransaction.populate(tx.message, [signature]) |
| #816 | |
| #817 | tx_encoded = base58.b58encode(bytes(signed_tx)).decode() |
| #818 | return await self._send_bundle([tx_encoded]) |
| #819 | |
| #820 | return "" |
| #821 | |
| #822 | async def health_check(self) -> bool: |
| #823 | """Check if Bags API is healthy.""" |
| #824 | try: |
| #825 | response = await self._client.get("/../../ping") |
| #826 | data = response.json() |
| #827 | return data.get("message") == "pong" |
| #828 | except Exception: |
| #829 | return False |
| #830 | |
| #831 | async def close(self): |
| #832 | """Close the HTTP client""" |
| #833 | await self._client.aclose() |
| #834 | |
| #835 | async def __aenter__(self): |
| #836 | return self |
| #837 | |
| #838 | async def __aexit__(self, exc_type, exc_val, exc_tb): |
| #839 | await self.close() |
| #840 |