diff --git a/buy.py b/buy.py new file mode 100644 index 0000000..0d39617 --- /dev/null +++ b/buy.py @@ -0,0 +1,277 @@ +import asyncio +import json +import base64 +import struct +import base58 +import hashlib +import websockets +import time + +from solana.rpc.async_api import AsyncClient +from solana.transaction import Transaction +from solana.rpc.commitment import Confirmed +from solana.rpc.types import TxOpts + +from solders.pubkey import Pubkey +from solders.keypair import Keypair +from solders.instruction import Instruction, AccountMeta +from solders.system_program import TransferParams, transfer +from solders.transaction import VersionedTransaction + +from spl.token.instructions import get_associated_token_address +import spl.token.instructions as spl_token + +from config import * + +from construct import Struct, Int64ul, Flag + +# Here and later all the discriminators are precalculated. See learning-examples/discriminator.py +EXPECTED_DISCRIMINATOR = struct.pack(" None: + parsed = self._STRUCT.parse(data[8:]) + self.__dict__.update(parsed) + +async def get_pump_curve_state(conn: AsyncClient, curve_address: Pubkey) -> BondingCurveState: + response = await conn.get_account_info(curve_address) + if not response.value or not response.value.data: + raise ValueError("Invalid curve state: No data") + + data = response.value.data + if data[:8] != EXPECTED_DISCRIMINATOR: + raise ValueError("Invalid curve state discriminator") + + return BondingCurveState(data) + +def calculate_pump_curve_price(curve_state: BondingCurveState) -> float: + if curve_state.virtual_token_reserves <= 0 or curve_state.virtual_sol_reserves <= 0: + raise ValueError("Invalid reserve state") + + return (curve_state.virtual_sol_reserves / LAMPORTS_PER_SOL) / (curve_state.virtual_token_reserves / 10 ** TOKEN_DECIMALS) + +async def buy_token(mint: Pubkey, bonding_curve: Pubkey, associated_bonding_curve: Pubkey, amount: float, slippage: float = 0.01, max_retries=5): + private_key = base58.b58decode(PRIVATE_KEY) + payer = Keypair.from_bytes(private_key) + + async with AsyncClient(RPC_ENDPOINT) as client: + associated_token_account = get_associated_token_address(payer.pubkey(), mint) + amount_lamports = int(amount * LAMPORTS_PER_SOL) + + # Fetch the token price + curve_state = await get_pump_curve_state(client, bonding_curve) + token_price_sol = calculate_pump_curve_price(curve_state) + token_amount = amount / token_price_sol + + # Calculate maximum SOL to spend with slippage + max_amount_lamports = int(amount_lamports * (1 + slippage)) + + # Create associated token account with retries + for ata_attempt in range(max_retries): + try: + account_info = await client.get_account_info(associated_token_account) + if account_info.value is None: + print(f"Creating associated token account (Attempt {ata_attempt + 1})...") + create_ata_ix = spl_token.create_associated_token_account( + payer=payer.pubkey(), + owner=payer.pubkey(), + mint=mint + ) + create_ata_tx = Transaction() + create_ata_tx.add(create_ata_ix) + recent_blockhash = await client.get_latest_blockhash() + create_ata_tx.recent_blockhash = recent_blockhash.value.blockhash + await client.send_transaction(create_ata_tx, payer) + print("Associated token account created.") + print(f"Associated token account address: {associated_token_account}") + break + else: + print("Associated token account already exists.") + print(f"Associated token account address: {associated_token_account}") + break + except Exception as e: + print(f"Attempt {ata_attempt + 1} to create associated token account failed: {str(e)}") + if ata_attempt < max_retries - 1: + wait_time = 2 ** ata_attempt + print(f"Retrying in {wait_time} seconds...") + await asyncio.sleep(wait_time) + else: + print("Max retries reached. Unable to create associated token account.") + return + + # Continue with the buy transaction + for attempt in range(max_retries): + try: + accounts = [ + AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False), + AccountMeta(pubkey=PUMP_FEE, is_signer=False, is_writable=True), + AccountMeta(pubkey=mint, is_signer=False, is_writable=False), + AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), + AccountMeta(pubkey=associated_bonding_curve, is_signer=False, is_writable=True), + AccountMeta(pubkey=associated_token_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True), + AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), + AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False), + AccountMeta(pubkey=SYSTEM_RENT, is_signer=False, is_writable=False), + AccountMeta(pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False), + AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False), + ] + + discriminator = struct.pack(" ping_interval: + await websocket.ping() + last_ping_time = current_time + + response = await asyncio.wait_for(websocket.recv(), timeout=30) + data = json.loads(response) + + if 'method' in data and data['method'] == 'blockNotification': + if 'params' in data and 'result' in data['params']: + block_data = data['params']['result'] + if 'value' in block_data and 'block' in block_data['value']: + block = block_data['value']['block'] + if 'transactions' in block: + for tx in block['transactions']: + if isinstance(tx, dict) and 'transaction' in tx: + tx_data_decoded = base64.b64decode(tx['transaction'][0]) + transaction = VersionedTransaction.from_bytes(tx_data_decoded) + + for ix in transaction.message.instructions: + if str(transaction.message.account_keys[ix.program_id_index]) == str(PUMP_PROGRAM): + ix_data = bytes(ix.data) + discriminator = struct.unpack(' 0: + tx_signature = tx['transaction'][0] + elif isinstance(tx['transaction'], dict) and 'signatures' in tx['transaction']: + tx_signature = tx['transaction']['signatures'][0] + else: + continue + await save_transaction(tx, tx_signature) + elif 'result' in data: + print(f"Subscription confirmed") + except Exception as e: + print(f"An error occurred: {str(e)}") + +if __name__ == "__main__": + asyncio.run(listen_for_transactions()) \ No newline at end of file diff --git a/learning-examples/calculate_discriminator.py b/learning-examples/calculate_discriminator.py new file mode 100644 index 0000000..438262c --- /dev/null +++ b/learning-examples/calculate_discriminator.py @@ -0,0 +1,31 @@ +import hashlib +import struct + +# https://book.anchor-lang.com/anchor_bts/discriminator.html +# Set the instruction name here +instruction_name = "account:BondingCurve" + +def calculate_discriminator(instruction_name): + # Create a SHA256 hash object + sha = hashlib.sha256() + + # Update the hash with the instruction name + sha.update(instruction_name.encode('utf-8')) + + # Get the first 8 bytes of the hash + discriminator_bytes = sha.digest()[:8] + + # Convert the bytes to a 64-bit unsigned integer (little-endian) + discriminator = struct.unpack('") + sys.exit(1) + +tx_file_path = sys.argv[1] +idl = load_idl('../idl/pump_fun_idl.json') +tx_data = load_transaction(tx_file_path) + +decoded_instructions = decode_transaction(tx_data, idl) +print(json.dumps(decoded_instructions, indent=2)) \ No newline at end of file diff --git a/learning-examples/decode_from_getAccountInfo.py b/learning-examples/decode_from_getAccountInfo.py new file mode 100644 index 0000000..7dd5e1c --- /dev/null +++ b/learning-examples/decode_from_getAccountInfo.py @@ -0,0 +1,56 @@ +import json +import base64 +import struct +from construct import Struct, Int64ul, Flag + +LAMPORTS_PER_SOL = 1_000_000_000 +TOKEN_DECIMALS = 6 +EXPECTED_DISCRIMINATOR = struct.pack(" None: + parsed = self._STRUCT.parse(data[8:]) + self.__dict__.update(parsed) + +def calculate_bonding_curve_price(curve_state: BondingCurveState) -> float: + if curve_state.virtual_token_reserves <= 0 or curve_state.virtual_sol_reserves <= 0: + raise ValueError("Invalid reserve state") + + return (curve_state.virtual_sol_reserves / LAMPORTS_PER_SOL) / (curve_state.virtual_token_reserves / 10 ** TOKEN_DECIMALS) + +def decode_bonding_curve_data(raw_data: str) -> BondingCurveState: + decoded_data = base64.b64decode(raw_data) + if decoded_data[:8] != EXPECTED_DISCRIMINATOR: + raise ValueError("Invalid curve state discriminator") + return BondingCurveState(decoded_data) + +# Load the JSON data +with open('raw_bondingCurve_from_getAccountInfo.json', 'r') as file: + json_data = json.load(file) + +# Extract the base64 encoded data +encoded_data = json_data['result']['value']['data'][0] + +# Decode the data +bonding_curve_state = decode_bonding_curve_data(encoded_data) + +# Calculate and print the token price +token_price_sol = calculate_bonding_curve_price(bonding_curve_state) + +print("Bonding Curve State:") +print(f" Virtual Token Reserves: {bonding_curve_state.virtual_token_reserves}") +print(f" Virtual SOL Reserves: {bonding_curve_state.virtual_sol_reserves}") +print(f" Real Token Reserves: {bonding_curve_state.real_token_reserves}") +print(f" Real SOL Reserves: {bonding_curve_state.real_sol_reserves}") +print(f" Token Total Supply: {bonding_curve_state.token_total_supply}") +print(f" Complete: {bonding_curve_state.complete}") +print(f"\nToken Price: {token_price_sol:.10f} SOL") \ No newline at end of file diff --git a/learning-examples/decode_from_getTransaction.py b/learning-examples/decode_from_getTransaction.py new file mode 100644 index 0000000..519f146 --- /dev/null +++ b/learning-examples/decode_from_getTransaction.py @@ -0,0 +1,100 @@ +import json +import base58 +from solana.transaction import Transaction +from solders.pubkey import Pubkey +import struct +import sys +import base64 + +import sys + +if len(sys.argv) != 2: + print("Usage: python decode_getTransaction.py ") + sys.exit(1) + +tx_file_path = sys.argv[1] + +# Load the IDL +with open('../idl/pump_fun_idl.json', 'r') as f: + idl = json.load(f) + +# Load the transaction log +with open(tx_file_path, 'r') as f: + tx_log = json.load(f) + +# Extract the transaction data +tx_data = tx_log['result']['transaction'] + +print(json.dumps(tx_data, indent=2)) + +def decode_create_instruction(data): + # The Create instruction has 3 string arguments: name, symbol, uri + offset = 8 # Skip the 8-byte discriminator + results = [] + for _ in range(3): + length = struct.unpack_from(" None: + parsed = self._STRUCT.parse(data[8:]) + self.__dict__.update(parsed) + +async def get_bonding_curve_state(conn: AsyncClient, curve_address: Pubkey) -> BondingCurveState: + response = await conn.get_account_info(curve_address) + if not response.value or not response.value.data: + raise ValueError("Invalid curve state: No data") + + data = response.value.data + if data[:8] != EXPECTED_DISCRIMINATOR: + raise ValueError("Invalid curve state discriminator") + + return BondingCurveState(data) + +def calculate_bonding_curve_price(curve_state: BondingCurveState) -> float: + if curve_state.virtual_token_reserves <= 0 or curve_state.virtual_sol_reserves <= 0: + raise ValueError("Invalid reserve state") + + return (curve_state.virtual_sol_reserves / LAMPORTS_PER_SOL) / (curve_state.virtual_token_reserves / 10 ** TOKEN_DECIMALS) + +async def main() -> None: + try: + async with AsyncClient(RPC_ENDPOINT) as conn: + curve_address = Pubkey.from_string(CURVE_ADDRESS) + bonding_curve_state = await get_bonding_curve_state(conn, curve_address) + token_price_sol = calculate_bonding_curve_price(bonding_curve_state) + + print("Token price:") + print(f" {token_price_sol:.10f} SOL") + except ValueError as e: + print(f"Error: {e}") + except Exception as e: + print(f"An unexpected error occurred: {e}") + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/learning-examples/listen_create_from_blocksubscribe.py b/learning-examples/listen_create_from_blocksubscribe.py new file mode 100644 index 0000000..d3d726a --- /dev/null +++ b/learning-examples/listen_create_from_blocksubscribe.py @@ -0,0 +1,109 @@ +import asyncio +import json +import websockets +import base64 +import struct +import hashlib +import sys +import os +from solders.transaction import VersionedTransaction +from solders.pubkey import Pubkey + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) +from config import WSS_ENDPOINT, PUMP_PROGRAM + +def load_idl(file_path): + with open(file_path, 'r') as f: + return json.load(f) + +def decode_create_instruction(ix_data, ix_def, accounts): + args = {} + offset = 8 # Skip 8-byte discriminator + + for arg in ix_def['args']: + if arg['type'] == 'string': + length = struct.unpack_from(' None: + parsed = self._STRUCT.parse(data[8:]) + self.__dict__.update(parsed) + +async def get_pump_curve_state(conn: AsyncClient, curve_address: Pubkey) -> BondingCurveState: + response = await conn.get_account_info(curve_address) + if not response.value or not response.value.data: + raise ValueError("Invalid curve state: No data") + + data = response.value.data + if data[:8] != EXPECTED_DISCRIMINATOR: + raise ValueError("Invalid curve state discriminator") + + return BondingCurveState(data) + +def calculate_pump_curve_price(curve_state: BondingCurveState) -> float: + if curve_state.virtual_token_reserves <= 0 or curve_state.virtual_sol_reserves <= 0: + raise ValueError("Invalid reserve state") + + return (curve_state.virtual_sol_reserves / LAMPORTS_PER_SOL) / (curve_state.virtual_token_reserves / 10 ** TOKEN_DECIMALS) + +async def buy_token(mint: Pubkey, bonding_curve: Pubkey, associated_bonding_curve: Pubkey, amount: float, slippage: float = 0.01, max_retries=5): + private_key = base58.b58decode("SOLANA_PRIVATE_KEY") + payer = Keypair.from_bytes(private_key) + + async with AsyncClient(RPC_ENDPOINT) as client: + associated_token_account = get_associated_token_address(payer.pubkey(), mint) + amount_lamports = int(amount * LAMPORTS_PER_SOL) + + # Fetch the token price + curve_state = await get_pump_curve_state(client, bonding_curve) + token_price_sol = calculate_pump_curve_price(curve_state) + token_amount = amount / token_price_sol + + # Calculate maximum SOL to spend with slippage + max_amount_lamports = int(amount_lamports * (1 + slippage)) + + # Create associated token account with retries + for ata_attempt in range(max_retries): + try: + account_info = await client.get_account_info(associated_token_account) + if account_info.value is None: + print(f"Creating associated token account (Attempt {ata_attempt + 1})...") + create_ata_ix = spl_token.create_associated_token_account( + payer=payer.pubkey(), + owner=payer.pubkey(), + mint=mint + ) + create_ata_tx = Transaction() + create_ata_tx.add(create_ata_ix) + recent_blockhash = await client.get_latest_blockhash() + create_ata_tx.recent_blockhash = recent_blockhash.value.blockhash + await client.send_transaction(create_ata_tx, payer) + print("Associated token account created.") + print(f"Associated token account address: {associated_token_account}") + break + else: + print("Associated token account already exists.") + print(f"Associated token account address: {associated_token_account}") + break + except Exception as e: + print(f"Attempt {ata_attempt + 1} to create associated token account failed: {str(e)}") + if ata_attempt < max_retries - 1: + wait_time = 2 ** ata_attempt + print(f"Retrying in {wait_time} seconds...") + await asyncio.sleep(wait_time) + else: + print("Max retries reached. Unable to create associated token account.") + return + + # Continue with the buy transaction + for attempt in range(max_retries): + try: + accounts = [ + AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False), + AccountMeta(pubkey=PUMP_FEE, is_signer=False, is_writable=True), + AccountMeta(pubkey=mint, is_signer=False, is_writable=False), + AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), + AccountMeta(pubkey=associated_bonding_curve, is_signer=False, is_writable=True), + AccountMeta(pubkey=associated_token_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True), + AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), + AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False), + AccountMeta(pubkey=SYSTEM_RENT, is_signer=False, is_writable=False), + AccountMeta(pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False), + AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False), + ] + + discriminator = struct.pack(" None: + parsed = self._STRUCT.parse(data[8:]) + self.__dict__.update(parsed) + +async def get_pump_curve_state(conn: AsyncClient, curve_address: Pubkey) -> BondingCurveState: + response = await conn.get_account_info(curve_address) + if not response.value or not response.value.data: + raise ValueError("Invalid curve state: No data") + + data = response.value.data + if data[:8] != EXPECTED_DISCRIMINATOR: + raise ValueError("Invalid curve state discriminator") + + return BondingCurveState(data) + +def calculate_pump_curve_price(curve_state: BondingCurveState) -> float: + if curve_state.virtual_token_reserves <= 0 or curve_state.virtual_sol_reserves <= 0: + raise ValueError("Invalid reserve state") + + return (curve_state.virtual_sol_reserves / LAMPORTS_PER_SOL) / (curve_state.virtual_token_reserves / 10 ** TOKEN_DECIMALS) + +async def get_token_balance(conn: AsyncClient, associated_token_account: Pubkey): + response = await conn.get_token_account_balance(associated_token_account) + if response.value: + return int(response.value.amount) + return 0 + +async def sell_token(mint: Pubkey, bonding_curve: Pubkey, associated_bonding_curve: Pubkey, slippage: float = 0.25, max_retries=5): + private_key = base58.b58decode("SOLANA_PRIVATE_KEY") + payer = Keypair.from_bytes(private_key) + + async with AsyncClient(RPC_ENDPOINT) as client: + associated_token_account = get_associated_token_address(payer.pubkey(), mint) + + # Get token balance + token_balance = await get_token_balance(client, associated_token_account) + token_balance_decimal = token_balance / 10**TOKEN_DECIMALS + print(f"Token balance: {token_balance_decimal}") + if token_balance == 0: + print("No tokens to sell.") + return + + # Fetch the token price + curve_state = await get_pump_curve_state(client, bonding_curve) + token_price_sol = calculate_pump_curve_price(curve_state) + print(f"Price per Token: {token_price_sol:.20f} SOL") + + # Calculate minimum SOL output + amount = token_balance + min_sol_output = float(token_balance_decimal) * float(token_price_sol) + slippage_factor = 1 - slippage + min_sol_output = int((min_sol_output * slippage_factor) * LAMPORTS_PER_SOL) + + print(f"Selling {token_balance_decimal} tokens") + print(f"Minimum SOL output: {min_sol_output / LAMPORTS_PER_SOL:.10f} SOL") + + # Continue with the sell transaction + for attempt in range(max_retries): + try: + accounts = [ + AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False), + AccountMeta(pubkey=PUMP_FEE, is_signer=False, is_writable=True), + AccountMeta(pubkey=mint, is_signer=False, is_writable=False), + AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), + AccountMeta(pubkey=associated_bonding_curve, is_signer=False, is_writable=True), + AccountMeta(pubkey=associated_token_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True), + AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), + AccountMeta(pubkey=SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, is_signer=False, is_writable=False), + AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False), + AccountMeta(pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False), + AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False), + ] + + discriminator = struct.pack(" None: + parsed = self._STRUCT.parse(data[8:]) + self.__dict__.update(parsed) + +async def get_pump_curve_state(conn: AsyncClient, curve_address: Pubkey) -> BondingCurveState: + response = await conn.get_account_info(curve_address) + if not response.value or not response.value.data: + raise ValueError("Invalid curve state: No data") + + data = response.value.data + if data[:8] != EXPECTED_DISCRIMINATOR: + raise ValueError("Invalid curve state discriminator") + + return BondingCurveState(data) + +def calculate_pump_curve_price(curve_state: BondingCurveState) -> float: + if curve_state.virtual_token_reserves <= 0 or curve_state.virtual_sol_reserves <= 0: + raise ValueError("Invalid reserve state") + + return (curve_state.virtual_sol_reserves / LAMPORTS_PER_SOL) / (curve_state.virtual_token_reserves / 10 ** TOKEN_DECIMALS) + +async def get_token_balance(conn: AsyncClient, associated_token_account: Pubkey): + response = await conn.get_token_account_balance(associated_token_account) + if response.value: + return int(response.value.amount) + return 0 + +async def sell_token(mint: Pubkey, bonding_curve: Pubkey, associated_bonding_curve: Pubkey, slippage: float = 0.25, max_retries=5): + private_key = base58.b58decode(PRIVATE_KEY) + payer = Keypair.from_bytes(private_key) + + async with AsyncClient(RPC_ENDPOINT) as client: + associated_token_account = get_associated_token_address(payer.pubkey(), mint) + + # Get token balance + token_balance = await get_token_balance(client, associated_token_account) + token_balance_decimal = token_balance / 10**TOKEN_DECIMALS + print(f"Token balance: {token_balance_decimal}") + if token_balance == 0: + print("No tokens to sell.") + return + + # Fetch the token price + curve_state = await get_pump_curve_state(client, bonding_curve) + token_price_sol = calculate_pump_curve_price(curve_state) + print(f"Price per Token: {token_price_sol:.20f} SOL") + + # Calculate minimum SOL output + amount = token_balance + min_sol_output = float(token_balance_decimal) * float(token_price_sol) + slippage_factor = 1 - slippage + min_sol_output = int((min_sol_output * slippage_factor) * LAMPORTS_PER_SOL) + + print(f"Selling {token_balance_decimal} tokens") + print(f"Minimum SOL output: {min_sol_output / LAMPORTS_PER_SOL:.10f} SOL") + + for attempt in range(max_retries): + try: + accounts = [ + AccountMeta(pubkey=PUMP_GLOBAL, is_signer=False, is_writable=False), + AccountMeta(pubkey=PUMP_FEE, is_signer=False, is_writable=True), + AccountMeta(pubkey=mint, is_signer=False, is_writable=False), + AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True), + AccountMeta(pubkey=associated_bonding_curve, is_signer=False, is_writable=True), + AccountMeta(pubkey=associated_token_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True), + AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), + AccountMeta(pubkey=SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, is_signer=False, is_writable=False), + AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False), + AccountMeta(pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False), + AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False), + ] + + discriminator = struct.pack("