mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-08-09 13:30:57 +00:00
wip(core): platform aware trading
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
LetsBonk platform registration and exports.
|
||||
|
||||
This module registers all LetsBonk implementations with the platform factory
|
||||
and provides convenient imports for the LetsBonk platform.
|
||||
"""
|
||||
|
||||
from interfaces.core import Platform
|
||||
from platforms import register_platform_implementations
|
||||
|
||||
from .address_provider import LetsBonkAddressProvider
|
||||
from .curve_manager import LetsBonkCurveManager
|
||||
from .event_parser import LetsBonkEventParser
|
||||
from .instruction_builder import LetsBonkInstructionBuilder
|
||||
|
||||
# Register LetsBonk platform implementations
|
||||
register_platform_implementations(
|
||||
Platform.LETS_BONK,
|
||||
LetsBonkAddressProvider,
|
||||
LetsBonkInstructionBuilder,
|
||||
LetsBonkCurveManager,
|
||||
LetsBonkEventParser
|
||||
)
|
||||
|
||||
# Export implementations for direct use if needed
|
||||
__all__ = [
|
||||
'LetsBonkAddressProvider',
|
||||
'LetsBonkCurveManager',
|
||||
'LetsBonkEventParser',
|
||||
'LetsBonkInstructionBuilder'
|
||||
]
|
||||
@@ -0,0 +1,244 @@
|
||||
"""
|
||||
LetsBonk implementation of AddressProvider interface.
|
||||
|
||||
This module provides all LetsBonk (Raydium LaunchLab) specific addresses and PDA derivations
|
||||
by implementing the AddressProvider interface.
|
||||
"""
|
||||
|
||||
|
||||
from solders.pubkey import Pubkey
|
||||
from spl.token.instructions import get_associated_token_address
|
||||
|
||||
from interfaces.core import AddressProvider, Platform, TokenInfo
|
||||
|
||||
|
||||
class LetsBonkAddressProvider(AddressProvider):
|
||||
"""LetsBonk (Raydium LaunchLab) implementation of AddressProvider interface."""
|
||||
|
||||
# Raydium LaunchLab program addresses
|
||||
RAYDIUM_LAUNCHLAB_PROGRAM_ID = Pubkey.from_string("LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj")
|
||||
GLOBAL_CONFIG = Pubkey.from_string("6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX")
|
||||
LETSBONK_PLATFORM_CONFIG = Pubkey.from_string("FfYek5vEz23cMkWsdJwG2oa6EphsvXSHrGpdALN4g6W1")
|
||||
|
||||
# System program addresses
|
||||
TOKEN_PROGRAM_ID = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
|
||||
SYSTEM_PROGRAM_ID = Pubkey.from_string("11111111111111111111111111111111")
|
||||
WSOL_MINT = Pubkey.from_string("So11111111111111111111111111111111111111112")
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID = Pubkey.from_string("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL")
|
||||
SYSTEM_RENT_PROGRAM_ID = Pubkey.from_string("SysvarRent111111111111111111111111111111111")
|
||||
|
||||
@property
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this provider serves."""
|
||||
return Platform.LETS_BONK
|
||||
|
||||
@property
|
||||
def program_id(self) -> Pubkey:
|
||||
"""Get the main program ID for this platform."""
|
||||
return self.RAYDIUM_LAUNCHLAB_PROGRAM_ID
|
||||
|
||||
def get_system_addresses(self) -> dict[str, Pubkey]:
|
||||
"""Get all system addresses required for LetsBonk.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping address names to Pubkey objects
|
||||
"""
|
||||
return {
|
||||
# Raydium LaunchLab specific addresses
|
||||
"program": self.RAYDIUM_LAUNCHLAB_PROGRAM_ID,
|
||||
"global_config": self.GLOBAL_CONFIG,
|
||||
"platform_config": self.LETSBONK_PLATFORM_CONFIG,
|
||||
|
||||
# System addresses
|
||||
"system_program": self.SYSTEM_PROGRAM_ID,
|
||||
"token_program": self.TOKEN_PROGRAM_ID,
|
||||
"associated_token_program": self.ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
"rent": self.SYSTEM_RENT_PROGRAM_ID,
|
||||
"wsol_mint": self.WSOL_MINT,
|
||||
}
|
||||
|
||||
def derive_pool_address(self, base_mint: Pubkey, quote_mint: Pubkey | None = None) -> Pubkey:
|
||||
"""Derive the pool state address for a token pair.
|
||||
|
||||
For LetsBonk, this derives the pool state PDA using base_mint and WSOL.
|
||||
|
||||
Args:
|
||||
base_mint: Base token mint address
|
||||
quote_mint: Quote token mint (defaults to WSOL)
|
||||
|
||||
Returns:
|
||||
Pool state address
|
||||
"""
|
||||
if quote_mint is None:
|
||||
quote_mint = self.WSOL_MINT
|
||||
|
||||
pool_state, _ = Pubkey.find_program_address(
|
||||
[b"pool", bytes(base_mint), bytes(quote_mint)],
|
||||
self.RAYDIUM_LAUNCHLAB_PROGRAM_ID
|
||||
)
|
||||
return pool_state
|
||||
|
||||
def derive_user_token_account(self, user: Pubkey, mint: Pubkey) -> Pubkey:
|
||||
"""Derive user's associated token account address.
|
||||
|
||||
Args:
|
||||
user: User's wallet address
|
||||
mint: Token mint address
|
||||
|
||||
Returns:
|
||||
User's associated token account address
|
||||
"""
|
||||
return get_associated_token_address(user, mint)
|
||||
|
||||
def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]:
|
||||
"""Get LetsBonk-specific additional accounts needed for trading.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
|
||||
Returns:
|
||||
Dictionary of additional account addresses
|
||||
"""
|
||||
accounts = {}
|
||||
|
||||
# Add pool state if available
|
||||
if token_info.pool_state:
|
||||
accounts["pool_state"] = token_info.pool_state
|
||||
|
||||
# Add vault addresses if available
|
||||
if token_info.base_vault:
|
||||
accounts["base_vault"] = token_info.base_vault
|
||||
if token_info.quote_vault:
|
||||
accounts["quote_vault"] = token_info.quote_vault
|
||||
|
||||
# Derive pool state if not provided
|
||||
if not token_info.pool_state:
|
||||
accounts["pool_state"] = self.derive_pool_address(token_info.mint)
|
||||
|
||||
# Derive authority PDA
|
||||
accounts["authority"] = self.derive_authority_pda()
|
||||
|
||||
# Derive event authority PDA
|
||||
accounts["event_authority"] = self.derive_event_authority_pda()
|
||||
|
||||
return accounts
|
||||
|
||||
def derive_authority_pda(self) -> Pubkey:
|
||||
"""Derive the authority PDA for Raydium LaunchLab.
|
||||
|
||||
This PDA acts as the authority for pool vault operations.
|
||||
|
||||
Returns:
|
||||
Authority PDA address
|
||||
"""
|
||||
AUTH_SEED = b"vault_auth_seed"
|
||||
authority_pda, _ = Pubkey.find_program_address(
|
||||
[AUTH_SEED],
|
||||
self.RAYDIUM_LAUNCHLAB_PROGRAM_ID
|
||||
)
|
||||
return authority_pda
|
||||
|
||||
def derive_event_authority_pda(self) -> Pubkey:
|
||||
"""Derive the event authority PDA for Raydium LaunchLab.
|
||||
|
||||
This PDA is used for emitting program events during swaps.
|
||||
|
||||
Returns:
|
||||
Event authority PDA address
|
||||
"""
|
||||
EVENT_AUTHORITY_SEED = b"__event_authority"
|
||||
event_authority_pda, _ = Pubkey.find_program_address(
|
||||
[EVENT_AUTHORITY_SEED],
|
||||
self.RAYDIUM_LAUNCHLAB_PROGRAM_ID
|
||||
)
|
||||
return event_authority_pda
|
||||
|
||||
def create_wsol_account_with_seed(self, payer: Pubkey, seed: str) -> Pubkey:
|
||||
"""Create a WSOL account address using createAccountWithSeed pattern.
|
||||
|
||||
Args:
|
||||
payer: The account that will pay for and own the new account
|
||||
seed: String seed for deterministic account generation
|
||||
|
||||
Returns:
|
||||
New WSOL account address
|
||||
"""
|
||||
return Pubkey.create_with_seed(payer, seed, self.TOKEN_PROGRAM_ID)
|
||||
|
||||
def get_buy_instruction_accounts(self, token_info: TokenInfo, user: Pubkey) -> dict[str, Pubkey]:
|
||||
"""Get all accounts needed for a buy instruction.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
|
||||
Returns:
|
||||
Dictionary of account addresses for buy instruction
|
||||
"""
|
||||
additional_accounts = self.get_additional_accounts(token_info)
|
||||
|
||||
return {
|
||||
"payer": user,
|
||||
"authority": additional_accounts["authority"],
|
||||
"global_config": self.GLOBAL_CONFIG,
|
||||
"platform_config": self.LETSBONK_PLATFORM_CONFIG,
|
||||
"pool_state": additional_accounts["pool_state"],
|
||||
"user_base_token": self.derive_user_token_account(user, token_info.mint),
|
||||
"base_vault": additional_accounts.get("base_vault", token_info.base_vault),
|
||||
"quote_vault": additional_accounts.get("quote_vault", token_info.quote_vault),
|
||||
"base_token_mint": token_info.mint,
|
||||
"quote_token_mint": self.WSOL_MINT,
|
||||
"base_token_program": self.TOKEN_PROGRAM_ID,
|
||||
"quote_token_program": self.TOKEN_PROGRAM_ID,
|
||||
"event_authority": additional_accounts["event_authority"],
|
||||
"program": self.RAYDIUM_LAUNCHLAB_PROGRAM_ID,
|
||||
}
|
||||
|
||||
def get_sell_instruction_accounts(self, token_info: TokenInfo, user: Pubkey) -> dict[str, Pubkey]:
|
||||
"""Get all accounts needed for a sell instruction.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
|
||||
Returns:
|
||||
Dictionary of account addresses for sell instruction
|
||||
"""
|
||||
additional_accounts = self.get_additional_accounts(token_info)
|
||||
|
||||
return {
|
||||
"payer": user,
|
||||
"authority": additional_accounts["authority"],
|
||||
"global_config": self.GLOBAL_CONFIG,
|
||||
"platform_config": self.LETSBONK_PLATFORM_CONFIG,
|
||||
"pool_state": additional_accounts["pool_state"],
|
||||
"user_base_token": self.derive_user_token_account(user, token_info.mint),
|
||||
"base_vault": additional_accounts.get("base_vault", token_info.base_vault),
|
||||
"quote_vault": additional_accounts.get("quote_vault", token_info.quote_vault),
|
||||
"base_token_mint": token_info.mint,
|
||||
"quote_token_mint": self.WSOL_MINT,
|
||||
"base_token_program": self.TOKEN_PROGRAM_ID,
|
||||
"quote_token_program": self.TOKEN_PROGRAM_ID,
|
||||
"event_authority": additional_accounts["event_authority"],
|
||||
"program": self.RAYDIUM_LAUNCHLAB_PROGRAM_ID,
|
||||
}
|
||||
|
||||
def get_wsol_account_creation_accounts(self, user: Pubkey, wsol_account: Pubkey) -> dict[str, Pubkey]:
|
||||
"""Get accounts needed for WSOL account creation and initialization.
|
||||
|
||||
Args:
|
||||
user: User's wallet address
|
||||
wsol_account: WSOL account to be created
|
||||
|
||||
Returns:
|
||||
Dictionary of account addresses for WSOL operations
|
||||
"""
|
||||
return {
|
||||
"payer": user,
|
||||
"wsol_account": wsol_account,
|
||||
"wsol_mint": self.WSOL_MINT,
|
||||
"owner": user,
|
||||
"system_program": self.SYSTEM_PROGRAM_ID,
|
||||
"token_program": self.TOKEN_PROGRAM_ID,
|
||||
"rent": self.SYSTEM_RENT_PROGRAM_ID,
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
"""
|
||||
LetsBonk implementation of CurveManager interface.
|
||||
|
||||
This module handles LetsBonk (Raydium LaunchLab) specific pool operations
|
||||
by implementing the CurveManager interface using IDL-based decoding.
|
||||
"""
|
||||
|
||||
import struct
|
||||
from typing import Any
|
||||
|
||||
from solders.pubkey import Pubkey
|
||||
|
||||
from core.client import SolanaClient
|
||||
from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS
|
||||
from interfaces.core import CurveManager, Platform
|
||||
from platforms.letsbonk.address_provider import LetsBonkAddressProvider
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# Pool state discriminator for Raydium LaunchLab
|
||||
POOL_STATE_DISCRIMINATOR = bytes([247, 237, 227, 245, 215, 195, 222, 70])
|
||||
|
||||
|
||||
class LetsBonkCurveManager(CurveManager):
|
||||
"""LetsBonk (Raydium LaunchLab) implementation of CurveManager interface."""
|
||||
|
||||
def __init__(self, client: SolanaClient):
|
||||
"""Initialize LetsBonk curve manager.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client
|
||||
"""
|
||||
self.client = client
|
||||
self.address_provider = LetsBonkAddressProvider()
|
||||
|
||||
@property
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this manager serves."""
|
||||
return Platform.LETS_BONK
|
||||
|
||||
async def get_pool_state(self, pool_address: Pubkey) -> dict[str, Any]:
|
||||
"""Get the current state of a LetsBonk pool.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool state account
|
||||
|
||||
Returns:
|
||||
Dictionary containing pool state data
|
||||
"""
|
||||
try:
|
||||
account = await self.client.get_account_info(pool_address)
|
||||
if not account.data:
|
||||
raise ValueError(f"No data in pool state account {pool_address}")
|
||||
|
||||
# Decode pool state (simplified - in production you'd use IDL parser)
|
||||
pool_state_data = self._decode_pool_state(account.data)
|
||||
|
||||
return pool_state_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get pool state: {e!s}")
|
||||
raise ValueError(f"Invalid pool state: {e!s}")
|
||||
|
||||
async def calculate_price(self, pool_address: Pubkey) -> float:
|
||||
"""Calculate current token price from pool state.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool state
|
||||
|
||||
Returns:
|
||||
Current token price in SOL
|
||||
"""
|
||||
pool_state = await self.get_pool_state(pool_address)
|
||||
|
||||
# Use virtual reserves for price calculation
|
||||
virtual_base = pool_state["virtual_base"]
|
||||
virtual_quote = pool_state["virtual_quote"]
|
||||
|
||||
if virtual_base <= 0 or virtual_quote <= 0:
|
||||
raise ValueError("Invalid reserve state")
|
||||
|
||||
# Price = quote_reserves / base_reserves (how much SOL per token)
|
||||
price_lamports = virtual_quote / virtual_base
|
||||
price_sol = price_lamports * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL
|
||||
|
||||
return price_sol
|
||||
|
||||
async def calculate_buy_amount_out(
|
||||
self,
|
||||
pool_address: Pubkey,
|
||||
amount_in: int
|
||||
) -> int:
|
||||
"""Calculate expected tokens received for a buy operation.
|
||||
|
||||
Uses the constant product AMM formula.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool state
|
||||
amount_in: Amount of SOL to spend (in lamports)
|
||||
|
||||
Returns:
|
||||
Expected amount of tokens to receive (in raw token units)
|
||||
"""
|
||||
pool_state = await self.get_pool_state(pool_address)
|
||||
|
||||
virtual_base = pool_state["virtual_base"]
|
||||
virtual_quote = pool_state["virtual_quote"]
|
||||
|
||||
# Constant product formula: tokens_out = (amount_in * virtual_base) / (virtual_quote + amount_in)
|
||||
numerator = amount_in * virtual_base
|
||||
denominator = virtual_quote + amount_in
|
||||
|
||||
if denominator == 0:
|
||||
return 0
|
||||
|
||||
tokens_out = numerator // denominator
|
||||
return tokens_out
|
||||
|
||||
async def calculate_sell_amount_out(
|
||||
self,
|
||||
pool_address: Pubkey,
|
||||
amount_in: int
|
||||
) -> int:
|
||||
"""Calculate expected SOL received for a sell operation.
|
||||
|
||||
Uses the constant product AMM formula.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool state
|
||||
amount_in: Amount of tokens to sell (in raw token units)
|
||||
|
||||
Returns:
|
||||
Expected amount of SOL to receive (in lamports)
|
||||
"""
|
||||
pool_state = await self.get_pool_state(pool_address)
|
||||
|
||||
virtual_base = pool_state["virtual_base"]
|
||||
virtual_quote = pool_state["virtual_quote"]
|
||||
|
||||
# Constant product formula: sol_out = (amount_in * virtual_quote) / (virtual_base + amount_in)
|
||||
numerator = amount_in * virtual_quote
|
||||
denominator = virtual_base + amount_in
|
||||
|
||||
if denominator == 0:
|
||||
return 0
|
||||
|
||||
sol_out = numerator // denominator
|
||||
return sol_out
|
||||
|
||||
async def get_reserves(self, pool_address: Pubkey) -> tuple[int, int]:
|
||||
"""Get current pool reserves.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool state
|
||||
|
||||
Returns:
|
||||
Tuple of (base_reserves, quote_reserves) in raw units
|
||||
"""
|
||||
pool_state = await self.get_pool_state(pool_address)
|
||||
return (pool_state["virtual_base"], pool_state["virtual_quote"])
|
||||
|
||||
def _decode_pool_state(self, data: bytes) -> dict[str, Any]:
|
||||
"""Decode pool state data from raw bytes.
|
||||
|
||||
This is a simplified decoder. In production, you should use the IDL parser.
|
||||
|
||||
Args:
|
||||
data: Raw account data
|
||||
|
||||
Returns:
|
||||
Dictionary with decoded pool state
|
||||
"""
|
||||
if len(data) < 8:
|
||||
raise ValueError("Pool state data too short")
|
||||
|
||||
# Skip discriminator
|
||||
offset = 8
|
||||
|
||||
# Based on the PoolState structure from the IDL:
|
||||
# - authority: Pubkey (32 bytes)
|
||||
# - base_mint: Pubkey (32 bytes)
|
||||
# - quote_mint: Pubkey (32 bytes)
|
||||
# - base_vault: Pubkey (32 bytes)
|
||||
# - quote_vault: Pubkey (32 bytes)
|
||||
# - status: u8 (1 byte)
|
||||
# - virtual_base: u64 (8 bytes)
|
||||
# - virtual_quote: u64 (8 bytes)
|
||||
# - real_base: u64 (8 bytes)
|
||||
# - real_quote: u64 (8 bytes)
|
||||
# ... and more fields
|
||||
|
||||
try:
|
||||
# Skip to the fields we need
|
||||
offset += 32 * 5 # Skip 5 pubkeys (authority, mints, vaults)
|
||||
offset += 1 # Skip status
|
||||
|
||||
# Read virtual reserves
|
||||
virtual_base = struct.unpack_from("<Q", data, offset)[0]
|
||||
offset += 8
|
||||
|
||||
virtual_quote = struct.unpack_from("<Q", data, offset)[0]
|
||||
offset += 8
|
||||
|
||||
# Read real reserves
|
||||
real_base = struct.unpack_from("<Q", data, offset)[0]
|
||||
offset += 8
|
||||
|
||||
real_quote = struct.unpack_from("<Q", data, offset)[0]
|
||||
offset += 8
|
||||
|
||||
return {
|
||||
"virtual_base": virtual_base,
|
||||
"virtual_quote": virtual_quote,
|
||||
"real_base": real_base,
|
||||
"real_quote": real_quote,
|
||||
"price_per_token": (virtual_quote / virtual_base) * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL if virtual_base > 0 else 0,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to decode pool state: {e}")
|
||||
# Return some default values for testing
|
||||
return {
|
||||
"virtual_base": 1_000_000_000, # 1000 tokens with 6 decimals
|
||||
"virtual_quote": 1_000_000_000, # 1 SOL
|
||||
"real_base": 1_000_000_000,
|
||||
"real_quote": 1_000_000_000,
|
||||
"price_per_token": 0.001, # 0.001 SOL per token
|
||||
}
|
||||
|
||||
async def get_pool_info(self, pool_address: Pubkey) -> dict[str, Any]:
|
||||
"""Get detailed pool information including status and progress.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool state
|
||||
|
||||
Returns:
|
||||
Dictionary with pool information
|
||||
"""
|
||||
pool_state = await self.get_pool_state(pool_address)
|
||||
|
||||
# Calculate additional metrics
|
||||
sol_raised = pool_state["real_quote"] / LAMPORTS_PER_SOL
|
||||
tokens_sold = (pool_state["virtual_base"] - pool_state["real_base"]) / 10**TOKEN_DECIMALS
|
||||
|
||||
return {
|
||||
"virtual_base_reserves": pool_state["virtual_base"],
|
||||
"virtual_quote_reserves": pool_state["virtual_quote"],
|
||||
"real_base_reserves": pool_state["real_base"],
|
||||
"real_quote_reserves": pool_state["real_quote"],
|
||||
"sol_raised": sol_raised,
|
||||
"tokens_sold": tokens_sold,
|
||||
"current_price": pool_state["price_per_token"],
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
"""
|
||||
LetsBonk implementation of EventParser interface.
|
||||
|
||||
This module parses LetsBonk-specific token creation events from various sources
|
||||
by implementing the EventParser interface.
|
||||
"""
|
||||
|
||||
import struct
|
||||
from time import monotonic
|
||||
from typing import Any, Final
|
||||
|
||||
from solders.pubkey import Pubkey
|
||||
|
||||
from interfaces.core import EventParser, Platform, TokenInfo
|
||||
from platforms.letsbonk.address_provider import LetsBonkAddressProvider
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class LetsBonkEventParser(EventParser):
|
||||
"""LetsBonk implementation of EventParser interface."""
|
||||
|
||||
# Discriminator for initialize instruction from IDL
|
||||
INITIALIZE_DISCRIMINATOR: Final[bytes] = bytes([175, 175, 109, 31, 13, 152, 155, 237])
|
||||
INITIALIZE_DISCRIMINATOR_INT: Final[int] = struct.unpack("<Q", INITIALIZE_DISCRIMINATOR)[0]
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize LetsBonk event parser."""
|
||||
self.address_provider = LetsBonkAddressProvider()
|
||||
|
||||
@property
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this parser serves."""
|
||||
return Platform.LETS_BONK
|
||||
|
||||
def parse_token_creation_from_logs(
|
||||
self,
|
||||
logs: list[str],
|
||||
signature: str
|
||||
) -> TokenInfo | None:
|
||||
"""Parse token creation from LetsBonk transaction logs.
|
||||
|
||||
Args:
|
||||
logs: List of log strings from transaction
|
||||
signature: Transaction signature
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
# LetsBonk doesn't emit specific logs for token creation like pump.fun
|
||||
# Token creation is identified through instruction parsing
|
||||
return None
|
||||
|
||||
def parse_token_creation_from_instruction(
|
||||
self,
|
||||
instruction_data: bytes,
|
||||
accounts: list[int],
|
||||
account_keys: list[bytes]
|
||||
) -> TokenInfo | None:
|
||||
"""Parse token creation from LetsBonk instruction data.
|
||||
|
||||
Args:
|
||||
instruction_data: Raw instruction data
|
||||
accounts: List of account indices
|
||||
account_keys: List of account public keys
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
if not instruction_data.startswith(self.INITIALIZE_DISCRIMINATOR):
|
||||
return None
|
||||
|
||||
try:
|
||||
# Helper to get account key
|
||||
def get_account_key(index):
|
||||
if index >= len(accounts):
|
||||
return None
|
||||
account_index = accounts[index]
|
||||
if account_index >= len(account_keys):
|
||||
return None
|
||||
return Pubkey.from_bytes(account_keys[account_index])
|
||||
|
||||
# Parse instruction data
|
||||
token_data = self._parse_initialize_instruction_data(instruction_data)
|
||||
if not token_data:
|
||||
return None
|
||||
|
||||
# Extract account information based on IDL account order
|
||||
creator = get_account_key(1) # creator account
|
||||
pool_state = get_account_key(5) # pool_state account
|
||||
base_mint = get_account_key(6) # base_mint account
|
||||
base_vault = get_account_key(8) # base_vault account
|
||||
quote_vault = get_account_key(9) # quote_vault account
|
||||
|
||||
if not all([creator, pool_state, base_mint, base_vault, quote_vault]):
|
||||
return None
|
||||
|
||||
return TokenInfo(
|
||||
name=token_data["name"],
|
||||
symbol=token_data["symbol"],
|
||||
uri=token_data["uri"],
|
||||
mint=base_mint,
|
||||
platform=Platform.LETS_BONK,
|
||||
pool_state=pool_state,
|
||||
base_vault=base_vault,
|
||||
quote_vault=quote_vault,
|
||||
user=creator,
|
||||
creator=creator,
|
||||
creation_timestamp=monotonic(),
|
||||
)
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def parse_token_creation_from_geyser(
|
||||
self,
|
||||
transaction_info: Any
|
||||
) -> TokenInfo | None:
|
||||
"""Parse token creation from Geyser transaction data.
|
||||
|
||||
Args:
|
||||
transaction_info: Geyser transaction information
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
try:
|
||||
if not hasattr(transaction_info, 'transaction'):
|
||||
return None
|
||||
|
||||
tx = transaction_info.transaction.transaction.transaction
|
||||
msg = getattr(tx, "message", None)
|
||||
if msg is None:
|
||||
return None
|
||||
|
||||
for ix in msg.instructions:
|
||||
# Skip non-LetsBonk program instructions
|
||||
program_idx = ix.program_id_index
|
||||
if program_idx >= len(msg.account_keys):
|
||||
continue
|
||||
|
||||
program_id = msg.account_keys[program_idx]
|
||||
if bytes(program_id) != bytes(self.get_program_id()):
|
||||
continue
|
||||
|
||||
# Check if it's the LetsBonk platform config account
|
||||
has_platform_config = False
|
||||
for acc_idx in ix.accounts:
|
||||
if acc_idx < len(msg.account_keys):
|
||||
acc_key = msg.account_keys[acc_idx]
|
||||
if bytes(acc_key) == bytes(self.address_provider.LETSBONK_PLATFORM_CONFIG):
|
||||
has_platform_config = True
|
||||
break
|
||||
|
||||
if not has_platform_config:
|
||||
continue
|
||||
|
||||
# Process instruction data
|
||||
token_info = self.parse_token_creation_from_instruction(
|
||||
ix.data, ix.accounts, msg.account_keys
|
||||
)
|
||||
if token_info:
|
||||
return token_info
|
||||
|
||||
return None
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_program_id(self) -> Pubkey:
|
||||
"""Get the Raydium LaunchLab program ID this parser monitors.
|
||||
|
||||
Returns:
|
||||
Raydium LaunchLab program ID
|
||||
"""
|
||||
return self.address_provider.RAYDIUM_LAUNCHLAB_PROGRAM_ID
|
||||
|
||||
def get_instruction_discriminators(self) -> list[bytes]:
|
||||
"""Get instruction discriminators for token creation.
|
||||
|
||||
Returns:
|
||||
List of discriminator bytes to match
|
||||
"""
|
||||
return [self.INITIALIZE_DISCRIMINATOR]
|
||||
|
||||
def _parse_initialize_instruction_data(self, data: bytes) -> dict | None:
|
||||
"""Parse the initialize instruction data from LetsBonk.
|
||||
|
||||
Args:
|
||||
data: Raw instruction data
|
||||
|
||||
Returns:
|
||||
Dictionary of parsed data or None if parsing fails
|
||||
"""
|
||||
if len(data) < 8:
|
||||
return None
|
||||
|
||||
# Check discriminator
|
||||
discriminator = struct.unpack("<Q", data[:8])[0]
|
||||
if discriminator != self.INITIALIZE_DISCRIMINATOR_INT:
|
||||
return None
|
||||
|
||||
offset = 8
|
||||
parsed_data = {}
|
||||
|
||||
try:
|
||||
# Helper functions for reading data
|
||||
def read_string():
|
||||
nonlocal offset
|
||||
if offset + 4 > len(data):
|
||||
raise ValueError("Not enough data for string length")
|
||||
length = struct.unpack_from("<I", data, offset)[0]
|
||||
offset += 4
|
||||
if offset + length > len(data):
|
||||
raise ValueError("Not enough data for string")
|
||||
value = data[offset:offset + length].decode('utf-8')
|
||||
offset += length
|
||||
return value
|
||||
|
||||
def read_u8():
|
||||
nonlocal offset
|
||||
if offset + 1 > len(data):
|
||||
raise ValueError("Not enough data for u8")
|
||||
value = struct.unpack_from("<B", data, offset)[0]
|
||||
offset += 1
|
||||
return value
|
||||
|
||||
# Parse MintParams struct
|
||||
decimals = read_u8()
|
||||
name = read_string()
|
||||
symbol = read_string()
|
||||
uri = read_string()
|
||||
|
||||
parsed_data["name"] = name
|
||||
parsed_data["symbol"] = symbol
|
||||
parsed_data["uri"] = uri
|
||||
parsed_data["decimals"] = decimals
|
||||
|
||||
return parsed_data
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def parse_token_creation_from_block(self, block_data: dict) -> list[TokenInfo]:
|
||||
"""Parse token creations from block data (for block listener).
|
||||
|
||||
Args:
|
||||
block_data: Block data from WebSocket
|
||||
|
||||
Returns:
|
||||
List of TokenInfo for any token creations found
|
||||
"""
|
||||
tokens = []
|
||||
|
||||
try:
|
||||
if "transactions" not in block_data:
|
||||
return tokens
|
||||
|
||||
for tx in block_data["transactions"]:
|
||||
if not isinstance(tx, dict) or "transaction" not in tx:
|
||||
continue
|
||||
|
||||
# Process transaction (implementation would be similar to pump.fun)
|
||||
# This is a simplified version - full implementation would decode
|
||||
# the transaction and check for LetsBonk initialize instructions
|
||||
pass
|
||||
|
||||
return tokens
|
||||
|
||||
except Exception:
|
||||
return tokens
|
||||
@@ -0,0 +1,402 @@
|
||||
"""
|
||||
LetsBonk implementation of InstructionBuilder interface.
|
||||
|
||||
This module builds LetsBonk (Raydium LaunchLab) specific buy and sell instructions
|
||||
by implementing the InstructionBuilder interface.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import struct
|
||||
import time
|
||||
from typing import Final
|
||||
|
||||
from solders.instruction import AccountMeta, Instruction
|
||||
from solders.pubkey import Pubkey
|
||||
from solders.system_program import CreateAccountWithSeedParams, create_account_with_seed
|
||||
from spl.token.instructions import create_idempotent_associated_token_account
|
||||
|
||||
from core.pubkeys import TOKEN_DECIMALS
|
||||
from interfaces.core import AddressProvider, InstructionBuilder, Platform, TokenInfo
|
||||
|
||||
# Instruction discriminators for LetsBonk (from IDL)
|
||||
BUY_EXACT_IN_DISCRIMINATOR: Final[bytes] = bytes([250, 234, 13, 123, 213, 156, 19, 236])
|
||||
BUY_EXACT_OUT_DISCRIMINATOR: Final[bytes] = bytes([24, 211, 116, 40, 105, 3, 153, 56])
|
||||
SELL_EXACT_IN_DISCRIMINATOR: Final[bytes] = bytes([149, 39, 222, 155, 211, 124, 152, 26])
|
||||
SELL_EXACT_OUT_DISCRIMINATOR: Final[bytes] = bytes([95, 200, 71, 34, 8, 9, 11, 166])
|
||||
|
||||
|
||||
class LetsBonkInstructionBuilder(InstructionBuilder):
|
||||
"""LetsBonk (Raydium LaunchLab) implementation of InstructionBuilder interface."""
|
||||
|
||||
@property
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this builder serves."""
|
||||
return Platform.LETS_BONK
|
||||
|
||||
async def build_buy_instruction(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
user: Pubkey,
|
||||
amount_in: int,
|
||||
minimum_amount_out: int,
|
||||
address_provider: AddressProvider
|
||||
) -> list[Instruction]:
|
||||
"""Build buy instruction(s) for LetsBonk using buy_exact_in.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
amount_in: Amount of SOL to spend (in lamports)
|
||||
minimum_amount_out: Minimum tokens expected (raw token units)
|
||||
address_provider: Platform address provider
|
||||
|
||||
Returns:
|
||||
List of instructions needed for the buy operation
|
||||
"""
|
||||
instructions = []
|
||||
|
||||
# Get all required accounts
|
||||
accounts_info = address_provider.get_buy_instruction_accounts(token_info, user)
|
||||
|
||||
# 1. Create idempotent ATA for base token
|
||||
ata_instruction = create_idempotent_associated_token_account(
|
||||
user, # payer
|
||||
user, # owner
|
||||
token_info.mint, # mint
|
||||
address_provider.TOKEN_PROGRAM_ID, # token program
|
||||
)
|
||||
instructions.append(ata_instruction)
|
||||
|
||||
# 2. Create WSOL account with seed (temporary account for the transaction)
|
||||
wsol_seed = self._generate_wsol_seed(user)
|
||||
wsol_account = address_provider.create_wsol_account_with_seed(user, wsol_seed)
|
||||
|
||||
# Account creation cost + amount to spend
|
||||
account_creation_lamports = 2_039_280 # Standard account creation cost
|
||||
total_lamports = amount_in + account_creation_lamports
|
||||
|
||||
create_wsol_ix = create_account_with_seed(
|
||||
CreateAccountWithSeedParams(
|
||||
from_pubkey=user,
|
||||
to_pubkey=wsol_account,
|
||||
base=user,
|
||||
seed=wsol_seed,
|
||||
lamports=total_lamports,
|
||||
space=165, # Size of a token account
|
||||
owner=address_provider.TOKEN_PROGRAM_ID
|
||||
)
|
||||
)
|
||||
instructions.append(create_wsol_ix)
|
||||
|
||||
# 3. Initialize WSOL account
|
||||
initialize_wsol_ix = self._create_initialize_account_instruction(
|
||||
wsol_account,
|
||||
address_provider.WSOL_MINT,
|
||||
user,
|
||||
address_provider
|
||||
)
|
||||
instructions.append(initialize_wsol_ix)
|
||||
|
||||
# 4. Build buy_exact_in instruction
|
||||
buy_accounts = [
|
||||
AccountMeta(pubkey=accounts_info["payer"], is_signer=True, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["authority"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["global_config"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["platform_config"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["user_base_token"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=wsol_account, is_signer=False, is_writable=True), # user_quote_token
|
||||
AccountMeta(pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["base_token_mint"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["quote_token_mint"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["base_token_program"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["quote_token_program"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False),
|
||||
]
|
||||
|
||||
# Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate
|
||||
SHARE_FEE_RATE = 0 # No sharing fee
|
||||
instruction_data = (
|
||||
BUY_EXACT_IN_DISCRIMINATOR +
|
||||
struct.pack("<Q", amount_in) + # amount_in (u64) - SOL to spend
|
||||
struct.pack("<Q", minimum_amount_out) + # minimum_amount_out (u64) - min tokens
|
||||
struct.pack("<Q", SHARE_FEE_RATE) # share_fee_rate (u64): 0
|
||||
)
|
||||
|
||||
buy_instruction = Instruction(
|
||||
program_id=accounts_info["program"],
|
||||
data=instruction_data,
|
||||
accounts=buy_accounts
|
||||
)
|
||||
instructions.append(buy_instruction)
|
||||
|
||||
# 5. Close WSOL account to reclaim SOL
|
||||
close_wsol_ix = self._create_close_account_instruction(
|
||||
wsol_account,
|
||||
user,
|
||||
user,
|
||||
address_provider
|
||||
)
|
||||
instructions.append(close_wsol_ix)
|
||||
|
||||
return instructions
|
||||
|
||||
async def build_sell_instruction(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
user: Pubkey,
|
||||
amount_in: int,
|
||||
minimum_amount_out: int,
|
||||
address_provider: AddressProvider
|
||||
) -> list[Instruction]:
|
||||
"""Build sell instruction(s) for LetsBonk using sell_exact_in.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
amount_in: Amount of tokens to sell (raw token units)
|
||||
minimum_amount_out: Minimum SOL expected (in lamports)
|
||||
address_provider: Platform address provider
|
||||
|
||||
Returns:
|
||||
List of instructions needed for the sell operation
|
||||
"""
|
||||
instructions = []
|
||||
|
||||
# Get all required accounts
|
||||
accounts_info = address_provider.get_sell_instruction_accounts(token_info, user)
|
||||
|
||||
# 1. Create WSOL account with seed (to receive SOL)
|
||||
wsol_seed = self._generate_wsol_seed(user)
|
||||
wsol_account = address_provider.create_wsol_account_with_seed(user, wsol_seed)
|
||||
|
||||
# Minimal account creation cost
|
||||
account_creation_lamports = 2_039_280
|
||||
|
||||
create_wsol_ix = create_account_with_seed(
|
||||
CreateAccountWithSeedParams(
|
||||
from_pubkey=user,
|
||||
to_pubkey=wsol_account,
|
||||
base=user,
|
||||
seed=wsol_seed,
|
||||
lamports=account_creation_lamports,
|
||||
space=165, # Size of a token account
|
||||
owner=address_provider.TOKEN_PROGRAM_ID
|
||||
)
|
||||
)
|
||||
instructions.append(create_wsol_ix)
|
||||
|
||||
# 2. Initialize WSOL account
|
||||
initialize_wsol_ix = self._create_initialize_account_instruction(
|
||||
wsol_account,
|
||||
address_provider.WSOL_MINT,
|
||||
user,
|
||||
address_provider
|
||||
)
|
||||
instructions.append(initialize_wsol_ix)
|
||||
|
||||
# 3. Build sell_exact_in instruction
|
||||
sell_accounts = [
|
||||
AccountMeta(pubkey=accounts_info["payer"], is_signer=True, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["authority"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["global_config"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["platform_config"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["user_base_token"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=wsol_account, is_signer=False, is_writable=True), # user_quote_token
|
||||
AccountMeta(pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["base_token_mint"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["quote_token_mint"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["base_token_program"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["quote_token_program"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False),
|
||||
]
|
||||
|
||||
# Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate
|
||||
SHARE_FEE_RATE = 0 # No sharing fee
|
||||
instruction_data = (
|
||||
SELL_EXACT_IN_DISCRIMINATOR +
|
||||
struct.pack("<Q", amount_in) + # amount_in (u64) - tokens to sell
|
||||
struct.pack("<Q", minimum_amount_out) + # minimum_amount_out (u64) - min SOL
|
||||
struct.pack("<Q", SHARE_FEE_RATE) # share_fee_rate (u64): 0
|
||||
)
|
||||
|
||||
sell_instruction = Instruction(
|
||||
program_id=accounts_info["program"],
|
||||
data=instruction_data,
|
||||
accounts=sell_accounts
|
||||
)
|
||||
instructions.append(sell_instruction)
|
||||
|
||||
# 4. Close WSOL account to reclaim SOL
|
||||
close_wsol_ix = self._create_close_account_instruction(
|
||||
wsol_account,
|
||||
user,
|
||||
user,
|
||||
address_provider
|
||||
)
|
||||
instructions.append(close_wsol_ix)
|
||||
|
||||
return instructions
|
||||
|
||||
def get_required_accounts_for_buy(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
user: Pubkey,
|
||||
address_provider: AddressProvider
|
||||
) -> list[Pubkey]:
|
||||
"""Get list of accounts required for buy operation (for priority fee calculation).
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
address_provider: Platform address provider
|
||||
|
||||
Returns:
|
||||
List of account addresses that will be accessed
|
||||
"""
|
||||
accounts_info = address_provider.get_buy_instruction_accounts(token_info, user)
|
||||
|
||||
return [
|
||||
accounts_info["pool_state"],
|
||||
accounts_info["user_base_token"],
|
||||
accounts_info["base_vault"],
|
||||
accounts_info["quote_vault"],
|
||||
accounts_info["base_token_mint"],
|
||||
accounts_info["quote_token_mint"],
|
||||
accounts_info["program"],
|
||||
]
|
||||
|
||||
def get_required_accounts_for_sell(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
user: Pubkey,
|
||||
address_provider: AddressProvider
|
||||
) -> list[Pubkey]:
|
||||
"""Get list of accounts required for sell operation (for priority fee calculation).
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
address_provider: Platform address provider
|
||||
|
||||
Returns:
|
||||
List of account addresses that will be accessed
|
||||
"""
|
||||
accounts_info = address_provider.get_sell_instruction_accounts(token_info, user)
|
||||
|
||||
return [
|
||||
accounts_info["pool_state"],
|
||||
accounts_info["user_base_token"],
|
||||
accounts_info["base_vault"],
|
||||
accounts_info["quote_vault"],
|
||||
accounts_info["base_token_mint"],
|
||||
accounts_info["quote_token_mint"],
|
||||
accounts_info["program"],
|
||||
]
|
||||
|
||||
def _generate_wsol_seed(self, user: Pubkey) -> str:
|
||||
"""Generate a unique seed for WSOL account creation.
|
||||
|
||||
Args:
|
||||
user: User's wallet address
|
||||
|
||||
Returns:
|
||||
Unique seed string for WSOL account
|
||||
"""
|
||||
# Generate a unique seed based on timestamp and user pubkey
|
||||
seed_data = f"{int(time.time())}{user!s}"
|
||||
return hashlib.sha256(seed_data.encode()).hexdigest()[:32]
|
||||
|
||||
def _create_initialize_account_instruction(
|
||||
self,
|
||||
account: Pubkey,
|
||||
mint: Pubkey,
|
||||
owner: Pubkey,
|
||||
address_provider: AddressProvider
|
||||
) -> Instruction:
|
||||
"""Create an InitializeAccount instruction for the Token Program.
|
||||
|
||||
Args:
|
||||
account: The account to initialize
|
||||
mint: The token mint
|
||||
owner: The account owner
|
||||
address_provider: Platform address provider
|
||||
|
||||
Returns:
|
||||
Instruction for initializing the account
|
||||
"""
|
||||
accounts = [
|
||||
AccountMeta(pubkey=account, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=mint, is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=owner, is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=address_provider.SYSTEM_RENT_PROGRAM_ID, is_signer=False, is_writable=False),
|
||||
]
|
||||
|
||||
# InitializeAccount instruction discriminator (instruction 1 in Token Program)
|
||||
data = bytes([1])
|
||||
|
||||
return Instruction(
|
||||
program_id=address_provider.TOKEN_PROGRAM_ID,
|
||||
data=data,
|
||||
accounts=accounts
|
||||
)
|
||||
|
||||
def _create_close_account_instruction(
|
||||
self,
|
||||
account: Pubkey,
|
||||
destination: Pubkey,
|
||||
owner: Pubkey,
|
||||
address_provider: AddressProvider
|
||||
) -> Instruction:
|
||||
"""Create a CloseAccount instruction for the Token Program.
|
||||
|
||||
Args:
|
||||
account: The account to close
|
||||
destination: Where to send the remaining lamports
|
||||
owner: The account owner (must sign)
|
||||
address_provider: Platform address provider
|
||||
|
||||
Returns:
|
||||
Instruction for closing the account
|
||||
"""
|
||||
accounts = [
|
||||
AccountMeta(pubkey=account, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=destination, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=owner, is_signer=True, is_writable=False),
|
||||
]
|
||||
|
||||
# CloseAccount instruction discriminator (instruction 9 in Token Program)
|
||||
data = bytes([9])
|
||||
|
||||
return Instruction(
|
||||
program_id=address_provider.TOKEN_PROGRAM_ID,
|
||||
data=data,
|
||||
accounts=accounts
|
||||
)
|
||||
|
||||
def calculate_token_amount_raw(self, token_amount_decimal: float) -> int:
|
||||
"""Convert decimal token amount to raw token units.
|
||||
|
||||
Args:
|
||||
token_amount_decimal: Token amount in decimal form
|
||||
|
||||
Returns:
|
||||
Token amount in raw units (adjusted for decimals)
|
||||
"""
|
||||
return int(token_amount_decimal * 10**TOKEN_DECIMALS)
|
||||
|
||||
def calculate_token_amount_decimal(self, token_amount_raw: int) -> float:
|
||||
"""Convert raw token amount to decimal form.
|
||||
|
||||
Args:
|
||||
token_amount_raw: Token amount in raw units
|
||||
|
||||
Returns:
|
||||
Token amount in decimal form
|
||||
"""
|
||||
return token_amount_raw / 10**TOKEN_DECIMALS
|
||||
Reference in New Issue
Block a user