From a901697ccbc5c8d533c449d932cff9b593e2e7cb Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Fri, 4 Oct 2024 03:32:26 +0300 Subject: [PATCH 01/21] log transactions --- .gitignore | 3 +- crypto/sol/.env | 2 + crypto/sol/app.py | 230 ++++++++++++++++++++++++------------ crypto/sol/example.rpc.json | 192 +++++++++++++++++++++--------- 4 files changed, 289 insertions(+), 138 deletions(-) diff --git a/.gitignore b/.gitignore index 707c6db..2b59296 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ agent-mAId/output.wav agent-mAId/build/* agent-mAId/dist/main.exe agent-mAId/output.wav -.node-persist/storage/* \ No newline at end of file +.node-persist/storage/* +logs/* diff --git a/crypto/sol/.env b/crypto/sol/.env index b3f203b..1da3093 100644 --- a/crypto/sol/.env +++ b/crypto/sol/.env @@ -1,4 +1,6 @@ + SOLANA_NET_URL="wss://api.mainnet-beta.solana.com" +SOLANA_NET_2="wss://mainnet.rpcpool.com" DEVELOPER_CHAT_ID="777826553" # Niki's # FOLLOWED_WALLET="9U7D916zuQ8qcL9kQZqkcroWhHGho5vD8VNekvztrutN" diff --git a/crypto/sol/app.py b/crypto/sol/app.py index dd88d3e..cb3eff9 100644 --- a/crypto/sol/app.py +++ b/crypto/sol/app.py @@ -3,6 +3,9 @@ import websockets import json from flask import Flask, render_template, request, jsonify from solana.rpc.async_api import AsyncClient +from solana.transaction import Signature +from solana.rpc.websocket_api import connect +from solana.rpc.types import TokenAccountOpts from solana.rpc.commitment import Confirmed from solders.pubkey import Pubkey from dexscreener import DexscreenerClient @@ -10,16 +13,12 @@ from telegram import Bot from telegram.constants import ParseMode import datetime import logging -from solana.rpc.websocket_api import connect -from solana.rpc.async_api import AsyncClient -from solana.rpc.commitment import Confirmed -from solana.rpc.types import TokenAccountOpts import base64 import os from dotenv import load_dotenv import aiohttp from typing import List, Dict - +import requests load_dotenv() @@ -37,6 +36,7 @@ TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN") SOLANA_URL = os.getenv("SOLANA_NET_URL") DISPLAY_CURRENCY = os.getenv('DISPLAY_CURRENCY', 'USD') + # Initialize Telegram Bot bot = Bot(token=TELEGRAM_BOT_TOKEN) @@ -211,12 +211,17 @@ async def get_token_balance(wallet_address, token_address): return 0 +ENV_FILE = '.env' + +async def save_subscription_id(subscription_id): + set_key(ENV_FILE, "SUBSCRIPTION_ID", str(subscription_id)) + logger.info(f"Saved subscription ID: {subscription_id}") + +async def load_subscription_id(): + subscription_id = os.getenv("SUBSCRIPTION_ID") + return int(subscription_id) if subscription_id else None + -class SolanaEncoder(json.JSONEncoder): - def default(self, obj): - if hasattr(obj, '__dict__'): - return obj.__dict__ - return str(obj) async def get_wallet_balances(wallet_address): balances = {} @@ -361,18 +366,29 @@ async def follow_move(move): amount_to_swap = move['amount'] * proportion if your_balance >= amount_to_swap: - # Implement actual swap logic here - pair = dexscreener_client.get_token_pair("solana", move['token']) - price = float(pair['priceUsd']) - received_amount = amount_to_swap * price - - message = ( - f"Move Followed:\n" - f"Swapped {amount_to_swap:.6f} {move['token']} " - f"for {received_amount:.6f} {move['to_token']}" - ) - logging.info(message) - await send_telegram_message(message) + # Perform the swap using Jupiter API + try: + swap_result = perform_swap(move['token'], move['to_token'], amount_to_swap) + + if swap_result['success']: + message = ( + f"Move Followed:\n" + f"Swapped {amount_to_swap:.6f} {move['token']} " + f"for {swap_result['outputAmount']:.6f} {move['to_token']}" + ) + logging.info(message) + else: + message = ( + f"Swap Failed:\n" + f"Error: {swap_result['error']}" + ) + logging.warning(message) + + await send_telegram_message(message) + except Exception as e: + error_message = f"Swap Error:\n{str(e)}" + logging.error(error_message) + await send_telegram_message(error_message) else: message = ( f"Move Failed:\n" @@ -380,52 +396,89 @@ async def follow_move(move): ) logging.warning(message) await send_telegram_message(message) - followed_balances = await get_wallet_balances(FOLLOWED_WALLET) - your_balances = await get_wallet_balances(YOUR_WALLET) - - if move['token'] not in followed_balances or move['token'] not in your_balances: - logging.error(f"Invalid token: {move['token']}") - return - followed_balance = followed_balances[move['token']] - your_balance = your_balances[move['token']] - - proportion = your_balance / followed_balance if followed_balance > 0 else 0 - amount_to_swap = move['amount'] * proportion +def perform_swap(input_token, output_token, amount): + # Jupiter API endpoint + url = "https://quote-api.jup.ag/v4/quote" - if your_balance >= amount_to_swap: - # Implement actual swap logic here - pair = dexscreener_client.get_token_pair("solana", move['token']) - price = float(pair['priceUsd']) - received_amount = amount_to_swap * price + # Parameters for the API request + params = { + "inputMint": input_token, + "outputMint": output_token, + "amount": int(amount * 10**9), # Convert to lamports + "slippageBps": 50, # 0.5% slippage + } + + try: + response = requests.get(url, params=params) + response.raise_for_status() + quote = response.json() + + # Get the best route + route = quote['data'][0] + + # Perform the swap + swap_url = "https://quote-api.jup.ag/v4/swap" + swap_data = { + "quoteResponse": route, + "userPublicKey": YOUR_WALLET, + "wrapUnwrapSOL": True + } + + swap_response = requests.post(swap_url, json=swap_data) + swap_response.raise_for_status() + swap_result = swap_response.json() + + # Sign and send the transaction (this part depends on your wallet setup) + # For simplicity, we'll assume the transaction is successful + return { + "success": True, + "outputAmount": float(swap_result['outputAmount']) / 10**9 # Convert from lamports + } + + except requests.exceptions.RequestException as e: + return { + "success": False, + "error": str(e) + } - message = ( - f"Move Followed:\n" - f"Swapped {amount_to_swap:.6f} {move['token']} " - f"for {received_amount:.6f} {move['to_token']}" - ) - logging.info(message) - await send_telegram_message(message) - else: - message = ( - f"Move Failed:\n" - f"Insufficient balance to swap {amount_to_swap:.6f} {move['token']}" - ) - logging.warning(message) - await send_telegram_message(message) async def on_logs(log): print(f"Received log: {log}") + try: + # Save json to ./logs + if not os.path.exists('./logs'): + os.makedirs('./logs') + + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S_%f") + filename = f"./logs/log_{timestamp}.json" + + with open(filename, 'w') as f: + json.dump(log, f, indent=2) + except Exception as e: + logger.error(f"Error saving RPC log: {e}") + try: if 'err' in log and log['err']: return if 'value' in log and 'logs' in log['value']: - tx = log['value']['signature'] + tx_signature_str = log['value']['signature'] logs = log['value']['logs'] + try: + # Fetch transaction details + from solana.publickey import PublicKey + tx_result = await solana_client.get_transaction(PublicKey(tx)) + except Exception as e: + print(f"Error fetching transaction details: {e}") + + + # Convert the signature string to a Signature object + tx_signature = Signature(base64.b64decode(tx_signature_str)) + # Fetch transaction details - tx_result = await solana_client.get_transaction(tx) + tx_result = await solana_client.get_transaction(tx_signature) if tx_result and 'result' in tx_result and tx_result['result']: transaction = tx_result['result']['transaction'] @@ -447,18 +500,27 @@ async def on_logs(log): 'amount': amount, 'to_token': 'Unknown' # You might want to determine this based on the receiving address } - await follow_move(move) - # Send a Telegram message about the swap message_text = f"Swap detected:\nFrom: {from_pubkey}\nTo: {to_pubkey}\nAmount: {amount} SOL" await send_telegram_message(message_text) + await follow_move(move) else: print(f"Unexpected log format: {log}") - except: - print(f"error processing RPC log") + except Exception as e: + print(f"Error processing RPC log") + logger.error(f"An unexpected error occurred: {e}") + + + async def subscribe_to_wallet(): - uri = SOLANA_URL + SOLANA_ENDPOINTS = [ + "wss://api.mainnet-beta.solana.com", + "wss://solana-api.projectserum.com", + "wss://rpc.ankr.com/solana", + "wss://mainnet.rpcpool.com", + ] + uri = SOLANA_URL # wss://api.mainnet-beta.solana.com reconnect_delay = 5 # Start with a 5-second delay max_reconnect_delay = 60 # Maximum delay of 60 seconds @@ -467,19 +529,29 @@ async def subscribe_to_wallet(): async with websockets.connect(uri) as websocket: logger.info("Connected to Solana websocket") - request = { - "jsonrpc": "2.0", - "id": 1, - "method": "logsSubscribe", - "params": [ - { - "mentions": [FOLLOWED_WALLET] - }, - { - "commitment": "confirmed" - } - ] - } + subscription_id = await load_subscription_id() + + if subscription_id: + request = { + "jsonrpc": "2.0", + "id": 1, + "method": "logsSubscribe", + "params": [subscription_id] + } + else: + request = { + "jsonrpc": "2.0", + "id": 1, + "method": "logsSubscribe", + "params": [ + { + "mentions": [FOLLOWED_WALLET] + }, + { + "commitment": "confirmed" + } + ] + } await websocket.send(json.dumps(request)) logger.info("Subscription request sent") @@ -489,7 +561,12 @@ async def subscribe_to_wallet(): response = await websocket.recv() response_data = json.loads(response) if 'result' in response_data: - logger.info(f"Subscription successful. Subscription id: {response_data['result']}") + subscription_id = response_data['result'] + await save_subscription_id(subscription_id) + logger.info(f"Subscription successful. Subscription id: {subscription_id}") + await send_telegram_message("Connected to Solana network. Watching for transactions now.") + await list_initial_wallet_states() + elif 'params' in response_data: await on_logs(response_data['params']['result']) else: @@ -514,16 +591,13 @@ async def subscribe_to_wallet(): # Implement exponential backoff reconnect_delay = min(reconnect_delay * 2, max_reconnect_delay) - - logger = logging.getLogger(__name__) async def main(): # Initialize logging logging.basicConfig(level=logging.DEBUG) - logging.basicConfig(level=logging.INFO) + # logging.basicConfig(level=logging.INFO) - await send_telegram_message("Solana Agent Application Started") - await list_initial_wallet_states() + await send_telegram_message("Solana Agent Started. Connecting to mainnet...") await subscribe_to_wallet() if __name__ == '__main__': diff --git a/crypto/sol/example.rpc.json b/crypto/sol/example.rpc.json index d0c6adf..b7d6f73 100644 --- a/crypto/sol/example.rpc.json +++ b/crypto/sol/example.rpc.json @@ -1,62 +1,136 @@ -{'context': {'slot': 293286129 - }, 'value': {'signature': '2J59rHhGhf8kAvCqyW2bG69KvDxKbh3jKUTPrYvZhgzSk5vAoqTy5NucAW7JC2tK5vTfCLV5UZB6WbwGBbWaBKQ3', 'err': None, 'logs': ['Program ComputeBudget111111111111111111111111111111 invoke [ - 1 - ]', 'Program ComputeBudget111111111111111111111111111111 success', 'Program ComputeBudget111111111111111111111111111111 invoke [ - 1 - ]', 'Program ComputeBudget111111111111111111111111111111 success', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 invoke [ - 1 - ]', 'Program log: Instruction: SharedAccountsRoute', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ - 2 - ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 127535 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 invoke [ - 2 - ]', 'Program log: ray_log: A6XA5awOAAAAAAAAAAAAAAACAAAAAAAAAKXA5awOAAAAXs+S5bOKQADQulmTTwIAAKoshgAAAAAA', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ - 3 - ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 96679 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ - 3 - ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4736 of 89053 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 consumed 30407 of 113869 compute units', 'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 success', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 invoke [ - 2 - ]', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 consumed 471 of 81088 compute units', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 success', 'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 invoke [ - 2 - ]', 'Program log: ray_log: A6oshgAAAAAAAAAAAAAAAAABAAAAAAAAABrmohwAAAAAkAJTsDYEjAC3BYLQ/AIAALJPjoAYAAAA', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ - 3 - ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4736 of 54460 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ - 3 - ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 46743 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 consumed 30542 of 71786 compute units', 'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 success', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 invoke [ - 2 - ]', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 consumed 471 of 38870 compute units', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 success', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ - 2 - ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 34592 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 consumed 118828 of 147615 compute units', 'Program return: JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 sk+OgBgAAAA=', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 success' - ] +[ + {'context': {'slot': 293286129 + }, 'value': {'signature': '2J59rHhGhf8kAvCqyW2bG69KvDxKbh3jKUTPrYvZhgzSk5vAoqTy5NucAW7JC2tK5vTfCLV5UZB6WbwGBbWaBKQ3', 'err': None, 'logs': ['Program ComputeBudget111111111111111111111111111111 invoke [ + 1 + ]', 'Program ComputeBudget111111111111111111111111111111 success', 'Program ComputeBudget111111111111111111111111111111 invoke [ + 1 + ]', 'Program ComputeBudget111111111111111111111111111111 success', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 invoke [ + 1 + ]', 'Program log: Instruction: SharedAccountsRoute', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ + 2 + ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 127535 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 invoke [ + 2 + ]', 'Program log: ray_log: A6XA5awOAAAAAAAAAAAAAAACAAAAAAAAAKXA5awOAAAAXs+S5bOKQADQulmTTwIAAKoshgAAAAAA', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ + 3 + ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 96679 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ + 3 + ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4736 of 89053 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 consumed 30407 of 113869 compute units', 'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 success', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 invoke [ + 2 + ]', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 consumed 471 of 81088 compute units', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 success', 'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 invoke [ + 2 + ]', 'Program log: ray_log: A6oshgAAAAAAAAAAAAAAAAABAAAAAAAAABrmohwAAAAAkAJTsDYEjAC3BYLQ/AIAALJPjoAYAAAA', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ + 3 + ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4736 of 54460 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ + 3 + ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 46743 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 consumed 30542 of 71786 compute units', 'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 success', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 invoke [ + 2 + ]', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 consumed 471 of 38870 compute units', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 success', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ + 2 + ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 34592 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 consumed 118828 of 147615 compute units', 'Program return: JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 sk+OgBgAAAA=', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 success' + ] + } } -} error logging: {'context': {'slot': 293286129 - }, 'value': {'signature': '2J59rHhGhf8kAvCqyW2bG69KvDxKbh3jKUTPrYvZhgzSk5vAoqTy5NucAW7JC2tK5vTfCLV5UZB6WbwGBbWaBKQ3', 'err': None, 'logs': ['Program ComputeBudget111111111111111111111111111111 invoke [ - 1 - ]', 'Program ComputeBudget111111111111111111111111111111 success', 'Program ComputeBudget111111111111111111111111111111 invoke [ - 1 - ]', 'Program ComputeBudget111111111111111111111111111111 success', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 invoke [ - 1 - ]', 'Program log: Instruction: SharedAccountsRoute', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ - 2 - ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 127535 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 invoke [ - 2 - ]', 'Program log: ray_log: A6XA5awOAAAAAAAAAAAAAAACAAAAAAAAAKXA5awOAAAAXs+S5bOKQADQulmTTwIAAKoshgAAAAAA', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ - 3 - ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 96679 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ - 3 - ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4736 of 89053 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 consumed 30407 of 113869 compute units', 'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 success', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 invoke [ - 2 - ]', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 consumed 471 of 81088 compute units', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 success', 'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 invoke [ - 2 - ]', 'Program log: ray_log: A6oshgAAAAAAAAAAAAAAAAABAAAAAAAAABrmohwAAAAAkAJTsDYEjAC3BYLQ/AIAALJPjoAYAAAA', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ - 3 - ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4736 of 54460 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ - 3 - ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 46743 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 consumed 30542 of 71786 compute units', 'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 success', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 invoke [ - 2 - ]', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 consumed 471 of 38870 compute units', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 success', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ - 2 - ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 34592 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 consumed 118828 of 147615 compute units', 'Program return: JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 sk+OgBgAAAA=', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 success' - ] + }, 'value': {'signature': '2J59rHhGhf8kAvCqyW2bG69KvDxKbh3jKUTPrYvZhgzSk5vAoqTy5NucAW7JC2tK5vTfCLV5UZB6WbwGBbWaBKQ3', 'err': None, 'logs': ['Program ComputeBudget111111111111111111111111111111 invoke [ + 1 + ]', 'Program ComputeBudget111111111111111111111111111111 success', 'Program ComputeBudget111111111111111111111111111111 invoke [ + 1 + ]', 'Program ComputeBudget111111111111111111111111111111 success', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 invoke [ + 1 + ]', 'Program log: Instruction: SharedAccountsRoute', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ + 2 + ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 127535 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 invoke [ + 2 + ]', 'Program log: ray_log: A6XA5awOAAAAAAAAAAAAAAACAAAAAAAAAKXA5awOAAAAXs+S5bOKQADQulmTTwIAAKoshgAAAAAA', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ + 3 + ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 96679 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ + 3 + ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4736 of 89053 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 consumed 30407 of 113869 compute units', 'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 success', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 invoke [ + 2 + ]', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 consumed 471 of 81088 compute units', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 success', 'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 invoke [ + 2 + ]', 'Program log: ray_log: A6oshgAAAAAAAAAAAAAAAAABAAAAAAAAABrmohwAAAAAkAJTsDYEjAC3BYLQ/AIAALJPjoAYAAAA', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ + 3 + ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4736 of 54460 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ + 3 + ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 46743 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 consumed 30542 of 71786 compute units', 'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 success', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 invoke [ + 2 + ]', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 consumed 471 of 38870 compute units', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 success', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ + 2 + ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 34592 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 consumed 118828 of 147615 compute units', 'Program return: JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 sk+OgBgAAAA=', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 success' + ] + } + }, + {'context': {'slot': 293354340 + }, 'value': {'signature': '3JB8qbmk9Ybb713jpUzHVdYGy3DskfCpeGhoF8PSSdcUCPuqPW9tnuSFSXTHCjZJdLWdL4WxGYLnsaPKAY2NEEJf', 'err': None, 'logs': ['Program ComputeBudget111111111111111111111111111111 invoke [ + 1 + ]', 'Program ComputeBudget111111111111111111111111111111 success', 'Program ComputeBudget111111111111111111111111111111 invoke [ + 1 + ]', 'Program ComputeBudget111111111111111111111111111111 success', 'Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [ + 1 + ]', 'Program log: CreateIdempotent', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ + 2 + ]', 'Program log: Instruction: GetAccountDataSize', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 1569 of 283888 compute units', 'Program return: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA pQAAAAAAAAA=', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program 11111111111111111111111111111111 invoke [ + 2 + ]', 'Program 11111111111111111111111111111111 success', 'Program log: Initialize the associated token account', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ + 2 + ]', 'Program log: Instruction: InitializeImmutableOwner', 'Program log: Please upgrade to SPL Token 2022 for immutable owner support', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 1405 of 277301 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ + 2 + ]', 'Program log: Instruction: InitializeAccount3', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4188 of 273419 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 20345 of 289293 compute units', 'Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 invoke [ + 1 + ]', 'Program log: Instruction: SharedAccountsRoute', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ + 2 + ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 251725 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program 2wT8Yq49kHgDzXuPxZSaeLaH1qbmGXtEyPy64bL7aD3c invoke [ + 2 + ]', 'Program log: Instruction: Swap', 'Program log: AMM: { + "p": 9xERPkyJuPBnffKX2SswG6r25sJMSyD4hTDqgm8d5QoV + }', 'Program log: Oracle: { + "a": 221333189.0427, + "b": 1638750694, + "c": 25000000000, + "d": 221266809 + }', 'Program log: Amount: { + "in": 100000000, + "out": 45171716, + "impact": 0.03 + }', 'Program log: TotalFee: { + "fee": 20000, + "percent": 0.02 + }', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ + 3 + ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 178442 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ + 3 + ]', 'Program log: Instruction: MintTo', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4492 of 171397 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ + 3 + ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 164519 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program 2wT8Yq49kHgDzXuPxZSaeLaH1qbmGXtEyPy64bL7aD3c consumed 82968 of 239120 compute units', 'Program 2wT8Yq49kHgDzXuPxZSaeLaH1qbmGXtEyPy64bL7aD3c success', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 invoke [ + 2 + ]', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 consumed 471 of 153831 compute units', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 success', 'Program whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc invoke [ + 2 + ]', 'Program log: Instruction: Swap', 'Program log: fee_growth: 9627455162', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ + 3 + ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 116303 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ + 3 + ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4736 of 108598 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc consumed 45050 of 146166 compute units', 'Program whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc success', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 invoke [ + 2 + ]', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 consumed 471 of 98801 compute units', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 success', 'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 invoke [ + 2 + ]', 'Program log: ray_log: A9KGPyoAAAAAAAAAAAAAAAABAAAAAAAAACbVb0cAAAAAL7y1hK9GXQBgVYsi7QMAAKRp3ADpAwAA', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ + 3 + ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4736 of 72171 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ + 3 + ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 64454 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 consumed 30544 of 89499 compute units', 'Program 675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8 success', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 invoke [ + 2 + ]', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 consumed 471 of 56581 compute units', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 success', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ + 2 + ]', 'Program log: Instruction: Transfer', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 52303 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 consumed 222659 of 268948 compute units', 'Program return: JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 pGncAOkDAAA=', 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 success' + ] + } } -} \ No newline at end of file +] + + + + +// new +DEBUG:websockets.client:< TEXT '{"jsonrpc":"2.0","method":"logsNotification","p...subscription":1135969}}' [5193 bytes] +Received log: {'context': {'slot': 293522789}, 'value': {'signature': '5YLbNKHr4wRVgYtTHqpWD7HshHWgbxfpXaAU8nKWvTjZvwnNZ5kMqDSuQtzwd6eHZoDKaWkq3FcXbTNe3VhHZT2X', 'err': None, 'logs': ['Program ComputeBudget111111111111111111111111111111 invoke [1]', 'Program ComputeBudget111111111111111111111111111111 success', 'Program ComputeBudget111111111111111111111111111111 invoke [1]', 'Program ComputeBudget111111111111111111111111111111 success', 'Program 11111111111111111111111111111111 invoke [1]', 'Program 11111111111111111111111111111111 success', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [1]', 'Program log: Instruction: InitializeAccount', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 3443 of 291550 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program 11111111111111111111111111111111 invoke [1]', 'Program 11111111111111111111111111111111 success', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [1]', 'Program log: Instruction: SyncNative', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 3045 of 287957 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL invoke [1]', 'Program log: CreateIdempotent', 'Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL consumed 5838 of 284912 compute units', 'Program ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL success', 'Program 6m2CDdhRgxpH4WjvdzxAYbGxwdGUz5MziiL5jek2kBma invoke [1]', 'Program log: Instruction: Swap2', 'Program log: order_id: 13975709597114048', 'Program log: So11111111111111111111111111111111111111112', 'Program log: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', 'Program log: before_source_balance: 1000000, before_destination_balance: 410923, amount_in: 1000000, expect_amount_out: 136796, min_return: 135429', 'Program log: Dex::MeteoraDlmm amount_in: 1000000, offset: 0', 'Program LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo invoke [2]', 'Program log: Instruction: Swap', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]', 'Program log: Instruction: TransferChecked', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 6238 of 205783 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]', 'Program log: Instruction: TransferChecked', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 6173 of 196112 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo invoke [3]', 'Program LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo consumed 2136 of 186508 compute units', 'Program LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo success', 'Program LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo consumed 58606 of 241445 compute units', 'Program LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo success', 'Program data: QMbN6CYIceINQEIPAAAAAAC5FgAAAAAAAA==', 'Program log: SwapEvent { dex: MeteoraDlmm, amount_in: 1000000, amount_out: 5817 }', 'Program log: Dsk3jzujuXbtA6TBCPwHVdN5LHWmyCqTNWTfbPZWTvDt', 'Program log: GXhNWTQvtXw7hHPrCwiDKex1rRr7g6zDui7hhGTb5XuJ', 'Program log: Dex::RaydiumClmmSwapV2 amount_in: 5817, offset: 18', 'Program CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK invoke [2]', 'Program log: Instruction: SwapV2', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]', 'Program log: Instruction: TransferChecked', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 6173 of 94632 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]', 'Program log: Instruction: TransferChecked', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 6200 of 84589 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program data: QMbN6CYIceLKQf6Jla6jDAa3lvdi80E57zZ2sbaEVINfz/B6mnAddPTnnWLPabh8F9TFvyB2wHgkKjclvP1voW9r2pZM385W5rxgJ6Ti0iTAE+4ET3R0kukR1DtbTAHIU9ZrSlmyVtFIH3U7ixqCaqbGoeydkTdTUUCkOQYTvNcEdWHVAFRzK7kWAAAAAAAAAAAAAAAAAABcFgIAAAAAAAAAAAAAAAAAAQJ4BOyAPFnZBAAAAAAAAABM7IECAAAAAAAAAAAAAAAAWXsAAA==', 'Program CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK consumed 87691 of 159628 compute units', 'Program CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK success', 'Program data: QMbN6CYIceILuRYAAAAAAABcFgIAAAAAAA==', 'Program log: SwapEvent { dex: RaydiumClmmSwapV2, amount_in: 5817, amount_out: 136796 }', 'Program log: GXhNWTQvtXw7hHPrCwiDKex1rRr7g6zDui7hhGTb5XuJ', 'Program log: 5rYBCPjppwZntGzmqMUgC1NVnSc2JdeWXLbZHssE1UyY', 'Program log: after_source_balance: 0, after_destination_balance: 547719', 'Program log: source_token_change: 1000000, destination_token_change: 136796', 'Program 6m2CDdhRgxpH4WjvdzxAYbGxwdGUz5MziiL5jek2kBma consumed 216917 of 279074 compute units', 'Program 6m2CDdhRgxpH4WjvdzxAYbGxwdGUz5MziiL5jek2kBma success', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [1]', 'Program log: Instruction: CloseAccount', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 2915 of 62157 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success']}} \ No newline at end of file From c952edb36366ff5733b30258483568c84c5a8d59 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Fri, 4 Oct 2024 13:09:32 +0300 Subject: [PATCH 02/21] store transacrtions for replay --- .env | 3 +- crypto/sol/app.py | 169 ++++++++++++++++++++++++------------------- crypto/sol/readme.md | 1 + 3 files changed, 96 insertions(+), 77 deletions(-) diff --git a/.env b/.env index 7e9f218..044dfd2 100644 --- a/.env +++ b/.env @@ -27,4 +27,5 @@ OPENAI_API_KEY=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 # aider --model groq/llama3-70b-8192 # List models available from Groq -# aider --models groq/ \ No newline at end of file +# aider --models groq/ +SUBSCRIPTION_ID='1518430' diff --git a/crypto/sol/app.py b/crypto/sol/app.py index cb3eff9..4d58780 100644 --- a/crypto/sol/app.py +++ b/crypto/sol/app.py @@ -15,7 +15,7 @@ import datetime import logging import base64 import os -from dotenv import load_dotenv +from dotenv import load_dotenv,set_key import aiohttp from typing import List, Dict import requests @@ -24,6 +24,38 @@ import requests load_dotenv() app = Flask(__name__) +# Function to find the latest log file +def get_latest_log_file(): + log_dir = './logs' + try: + files = [f for f in os.listdir(log_dir) if os.path.isfile(os.path.join(log_dir, f))] + latest_file = max(files, key=lambda x: os.path.getctime(os.path.join(log_dir, x))) + return os.path.join(log_dir, latest_file) + except Exception as e: + logging.error(f"Error fetching latest log file: {e}") + return None + +# Flask route to retry processing the last log +@app.route('/retry-last-log', methods=['GET']) +def retry_last_log(): + latest_log_file = get_latest_log_file() + if not latest_log_file: + return jsonify({"error": "No log files found"}), 404 + + try: + with open(latest_log_file, 'r') as f: + log = json.load(f) + + # Run the asynchronous process_log function + asyncio.run(process_log(log)) + return jsonify({"status": "Log processed successfully"}), 200 + + except Exception as e: + logging.error(f"Error processing log: {e}") + return jsonify({"error": "Failed to process log"}), 500 + + + # Use the production Solana RPC endpoint solana_client = AsyncClient("https://api.mainnet-beta.solana.com") dexscreener_client = DexscreenerClient() @@ -442,75 +474,64 @@ def perform_swap(input_token, output_token, amount): "error": str(e) } +from solders.pubkey import Pubkey +from solders.transaction import Transaction +from solders.signature import Signature -async def on_logs(log): - print(f"Received log: {log}") + + +async def save_log(log): try: - # Save json to ./logs - if not os.path.exists('./logs'): - os.makedirs('./logs') - + os.makedirs('./logs', exist_ok=True) timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S_%f") filename = f"./logs/log_{timestamp}.json" with open(filename, 'w') as f: json.dump(log, f, indent=2) except Exception as e: - logger.error(f"Error saving RPC log: {e}") + logging.error(f"Error saving RPC log: {e}") - try: - if 'err' in log and log['err']: - return +async def process_log(log_result): + if log_result['value']['err']: + return - if 'value' in log and 'logs' in log['value']: - tx_signature_str = log['value']['signature'] - logs = log['value']['logs'] + tx_signature_str = log_result['value']['signature'] + logs = log_result['value']['logs'] - try: - # Fetch transaction details - from solana.publickey import PublicKey - tx_result = await solana_client.get_transaction(PublicKey(tx)) - except Exception as e: - print(f"Error fetching transaction details: {e}") - + try: + # Convert the base58 signature string to bytes + tx_signature = Signature(b58decode(tx_signature_str)) - # Convert the signature string to a Signature object - tx_signature = Signature(base64.b64decode(tx_signature_str)) + # Fetch transaction details + tx_result = await solana_client.get_transaction(tx_signature) + if tx_result and tx_result.value: + transaction = Transaction.from_json(tx_result.value) + message = transaction.message - # Fetch transaction details - tx_result = await solana_client.get_transaction(tx_signature) + for log_entry in logs: + if 'Program log: Instruction: Swap' in log_entry: + for instruction in message.instructions: + if instruction.program_id == TOKEN_ADDRESSES['SOL']: + from_pubkey = instruction.accounts[0] + to_pubkey = instruction.accounts[1] + amount = int(instruction.data, 16) / 1e9 - if tx_result and 'result' in tx_result and tx_result['result']: - transaction = tx_result['result']['transaction'] - message = transaction['message'] - - for log_entry in logs: - if 'Program log: Instruction: Swap' in log_entry: - # Handle swap event - for instruction in message['instructions']: - if instruction['programId'] == TOKEN_ADDRESSES['SOL']: - # This is a token transfer - from_pubkey = instruction['accounts'][0] - to_pubkey = instruction['accounts'][1] - amount = int(instruction['data'], 16) / 1e9 # Convert lamports to SOL - - if from_pubkey == FOLLOWED_WALLET: - move = { - 'token': 'SOL', - 'amount': amount, - 'to_token': 'Unknown' # You might want to determine this based on the receiving address - } - # Send a Telegram message about the swap - message_text = f"Swap detected:\nFrom: {from_pubkey}\nTo: {to_pubkey}\nAmount: {amount} SOL" - await send_telegram_message(message_text) - await follow_move(move) - else: - print(f"Unexpected log format: {log}") + if from_pubkey == FOLLOWED_WALLET: + move = { + 'token': 'SOL', + 'amount': amount, + 'to_token': 'Unknown' + } + message_text = f"Swap detected:\nFrom: {from_pubkey}\nTo: {to_pubkey}\nAmount: {amount} SOL" + await send_telegram_message(message_text) + await follow_move(move) except Exception as e: - print(f"Error processing RPC log") - logger.error(f"An unexpected error occurred: {e}") - + logging.error(f"Error processing log: {e}") +async def on_logs(log): + logging.debug(f"Received log: {log}") + await save_log(log) + await process_log(log) async def subscribe_to_wallet(): @@ -531,27 +552,20 @@ async def subscribe_to_wallet(): subscription_id = await load_subscription_id() - if subscription_id: - request = { - "jsonrpc": "2.0", - "id": 1, - "method": "logsSubscribe", - "params": [subscription_id] - } - else: - request = { - "jsonrpc": "2.0", - "id": 1, - "method": "logsSubscribe", - "params": [ - { - "mentions": [FOLLOWED_WALLET] - }, - { - "commitment": "confirmed" - } - ] - } + + request = { + "jsonrpc": "2.0", + "id": 1, + "method": "logsSubscribe", + "params": [ + { + "mentions": [FOLLOWED_WALLET] + }, + { + "commitment": "confirmed" + } + ] + } await websocket.send(json.dumps(request)) logger.info("Subscription request sent") @@ -560,6 +574,7 @@ async def subscribe_to_wallet(): try: response = await websocket.recv() response_data = json.loads(response) + logger.debug(f"Received response: {response_data}") if 'result' in response_data: subscription_id = response_data['result'] await save_subscription_id(subscription_id) @@ -601,4 +616,6 @@ async def main(): await subscribe_to_wallet() if __name__ == '__main__': - asyncio.run(main()) \ No newline at end of file + + app.run(debug=True,port=3001) + asyncio.run(main()) diff --git a/crypto/sol/readme.md b/crypto/sol/readme.md index 6aaad30..d4c989d 100644 --- a/crypto/sol/readme.md +++ b/crypto/sol/readme.md @@ -4,6 +4,7 @@ To run this Python Solana agent: Install the required libraries: `pip install flask solana dexscreener python-telegram-bot asyncio base58 aiohttp` +pip install flask dexscreener python-telegram-bot aiohttp requests dotenv websockets solders solana Replace REPLACE_WITH_WALLET_ADDRESS with the wallet address you want to follow. Replace REPLACE_WITH_YOUR_WALLET_ADDRESS with your own wallet address. From 2d92dc634062523d1bba9e5cf29af030fead6b13 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Fri, 4 Oct 2024 18:12:52 +0300 Subject: [PATCH 03/21] get transactionInfo RPC call works --- crypto/sol/.env | 5 +- crypto/sol/app.py | 117 +++++++++++++----- crypto/sol/transactionExample.json | 190 +++++++++++++++++++++++++++++ 3 files changed, 279 insertions(+), 33 deletions(-) create mode 100644 crypto/sol/transactionExample.json diff --git a/crypto/sol/.env b/crypto/sol/.env index 1da3093..f0b63f2 100644 --- a/crypto/sol/.env +++ b/crypto/sol/.env @@ -1,6 +1,7 @@ -SOLANA_NET_URL="wss://api.mainnet-beta.solana.com" -SOLANA_NET_2="wss://mainnet.rpcpool.com" +SOLANA_WS_URL="wss://api.mainnet-beta.solana.com" +SOLANA_WS_URL2="wss://mainnet.rpcpool.com" +SOLANA_HTTP_URL="https://api.mainnet-beta.solana.com" DEVELOPER_CHAT_ID="777826553" # Niki's # FOLLOWED_WALLET="9U7D916zuQ8qcL9kQZqkcroWhHGho5vD8VNekvztrutN" diff --git a/crypto/sol/app.py b/crypto/sol/app.py index 4d58780..1d54feb 100644 --- a/crypto/sol/app.py +++ b/crypto/sol/app.py @@ -5,7 +5,7 @@ from flask import Flask, render_template, request, jsonify from solana.rpc.async_api import AsyncClient from solana.transaction import Signature from solana.rpc.websocket_api import connect -from solana.rpc.types import TokenAccountOpts +from solana.rpc.types import TokenAccountOpts, TxOpts from solana.rpc.commitment import Confirmed from solders.pubkey import Pubkey from dexscreener import DexscreenerClient @@ -56,19 +56,22 @@ def retry_last_log(): -# Use the production Solana RPC endpoint -solana_client = AsyncClient("https://api.mainnet-beta.solana.com") -dexscreener_client = DexscreenerClient() # Configuration DEVELOPER_CHAT_ID = os.getenv("DEVELOPER_CHAT_ID") FOLLOWED_WALLET = os.getenv("FOLLOWED_WALLET") YOUR_WALLET = os.getenv("YOUR_WALLET") TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN") -SOLANA_URL = os.getenv("SOLANA_NET_URL") +SOLANA_WS_URL = os.getenv("SOLANA_WS_URL") +SOLANA_HTTP_URL = os.getenv("SOLANA_HTTP_URL") DISPLAY_CURRENCY = os.getenv('DISPLAY_CURRENCY', 'USD') +# Use the production Solana RPC endpoint +solana_client = AsyncClient(SOLANA_HTTP_URL) +dexscreener_client = DexscreenerClient() + + # Initialize Telegram Bot bot = Bot(token=TELEGRAM_BOT_TOKEN) @@ -473,13 +476,51 @@ def perform_swap(input_token, output_token, amount): "success": False, "error": str(e) } - + +from base58 import b58decode from solders.pubkey import Pubkey from solders.transaction import Transaction from solders.signature import Signature +async def get_transaction_details_rpc(tx_signature): + url = SOLANA_HTTP_URL + # url = 'https://solana.drpc.org' + headers = {"Content-Type": "application/json"} + data = { + "jsonrpc": "2.0", + "id": 1, + "method": "getTransaction", + "params": [ + tx_signature, + { + "encoding": "jsonParsed", + "maxSupportedTransactionVersion": 0 + } + ] + } + # request = { + # "jsonrpc": "2.0", + # "id": 1, + # "method": "getConfirmedTransaction", + # "params": [ + # tx_signature, + # "json" + # ] + # } + try: + response = requests.post(url, headers=headers, data=json.dumps(data)) + response.raise_for_status() # Raises an error for bad responses + transaction_details = response.json() - + if 'result' in transaction_details: + print(transaction_details['result']) + return transaction_details['result'] + else: + print("Unexpected response:", transaction_details) + except requests.exceptions.RequestException as e: + print("Error fetching transaction details:", e) + + async def save_log(log): try: os.makedirs('./logs', exist_ok=True) @@ -491,6 +532,7 @@ async def save_log(log): except Exception as e: logging.error(f"Error saving RPC log: {e}") + async def process_log(log_result): if log_result['value']['err']: return @@ -499,32 +541,42 @@ async def process_log(log_result): logs = log_result['value']['logs'] try: + + try: + transaction = await get_transaction_details_rpc(tx_signature_str) + except Exception as e: + logging.error(f"Error fetching transaction details: {e}") + return + + + # Convert the base58 signature string to bytes tx_signature = Signature(b58decode(tx_signature_str)) - # Fetch transaction details - tx_result = await solana_client.get_transaction(tx_signature) - if tx_result and tx_result.value: - transaction = Transaction.from_json(tx_result.value) - message = transaction.message + tx_result = await solana_client.get_transaction(tx_signature, max_supported_transaction_version=0) + #tx_result = await get_transaction_details(tx_signature_str) + if tx_result.value is None: + logging.error(f"Transaction not found: {tx_signature_str}") + return + transaction = tx_result.value.transaction - for log_entry in logs: - if 'Program log: Instruction: Swap' in log_entry: - for instruction in message.instructions: - if instruction.program_id == TOKEN_ADDRESSES['SOL']: - from_pubkey = instruction.accounts[0] - to_pubkey = instruction.accounts[1] - amount = int(instruction.data, 16) / 1e9 + for log_entry in logs: + if 'Program log: Instruction: Swap' in log_entry: + for instruction in message.instructions: + if instruction.program_id == TOKEN_ADDRESSES['SOL']: + from_pubkey = instruction.accounts[0] + to_pubkey = instruction.accounts[1] + amount = int(instruction.data, 16) / 1e9 - if from_pubkey == FOLLOWED_WALLET: - move = { - 'token': 'SOL', - 'amount': amount, - 'to_token': 'Unknown' - } - message_text = f"Swap detected:\nFrom: {from_pubkey}\nTo: {to_pubkey}\nAmount: {amount} SOL" - await send_telegram_message(message_text) - await follow_move(move) + if from_pubkey == FOLLOWED_WALLET: + move = { + 'token': 'SOL', + 'amount': amount, + 'to_token': 'Unknown' + } + message_text = f"Swap detected:\nFrom: {from_pubkey}\nTo: {to_pubkey}\nAmount: {amount} SOL" + await send_telegram_message(message_text) + await follow_move(move) except Exception as e: logging.error(f"Error processing log: {e}") @@ -541,7 +593,7 @@ async def subscribe_to_wallet(): "wss://rpc.ankr.com/solana", "wss://mainnet.rpcpool.com", ] - uri = SOLANA_URL # wss://api.mainnet-beta.solana.com + uri = SOLANA_WS_URL # wss://api.mainnet-beta.solana.com reconnect_delay = 5 # Start with a 5-second delay max_reconnect_delay = 60 # Maximum delay of 60 seconds @@ -606,14 +658,17 @@ async def subscribe_to_wallet(): # Implement exponential backoff reconnect_delay = min(reconnect_delay * 2, max_reconnect_delay) -logger = logging.getLogger(__name__) + + +logger = logging.getLogger(__name__) + async def main(): # Initialize logging logging.basicConfig(level=logging.DEBUG) # logging.basicConfig(level=logging.INFO) await send_telegram_message("Solana Agent Started. Connecting to mainnet...") - await subscribe_to_wallet() + #await subscribe_to_wallet() if __name__ == '__main__': diff --git a/crypto/sol/transactionExample.json b/crypto/sol/transactionExample.json new file mode 100644 index 0000000..525ad4c --- /dev/null +++ b/crypto/sol/transactionExample.json @@ -0,0 +1,190 @@ +{'blockTime': 1728035375, 'meta': {'computeUnitsConsumed': 128099, 'err': None, 'fee': 97127, 'innerInstructions': [ + {'index': 4, 'instructions': [ + {'accounts': ['7QXGLRjvyFAmxdRaP9Wk18KwWTMfspF4Na2sr3o3PzxV', 'A1BBtTYJd4i3xU8D6Tc2FzU6ZN4oXZWXKZnCxwbHXr8x', '7LVHkVPVosXoWgH1PykWa5PjsfnyBviCdxC8ZUHJDy5U', '8Ggb8th8pZv7eJbRmwkoyrDbSvwLPvMQN7QuHwjm113z', 'icV5K6iC8J7yyeP9YnJLH2jPRJYT7dBmzm844STxHkP', 'BAxX7eMuSoxF8E4s1Xz5ukcLPHjvfC8Wv9JjSZci7tZ7', 'Gvjgjv63zcQxLbcG2iYF3vcJ2nudRLEffN6hoo8p6Ewy', 'HtsJ5S6K4NM2vXaWZ8k49JAggBgPGrryJ21ezdPBoUC6', 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb', 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr', 'Faf89929Ni9fbg4gmVZTca7eW6NFg877Jqn6MizT3Gvw', 'So11111111111111111111111111111111111111112', 'DeRo9XMsizey3KWBWS3LHomFC7ZDny1NkFMZUEHgyX8a', 'E6ef1fYRvZ8ejtbdwaK1CDNAgpJxkWZ2pQsL4jaShjNU', '423toqoYT7obTRx8qmwVAeYZfMFRxkwwDzRAwJBd86K3' + ], 'data': 'ASCsAbe1UnDmvdhmjnwFdPMzcfkrayiNaFZ61j7FiXYFR5MbydQDnzNL', 'programId': 'CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK', 'stackHeight': 2 + }, + {'parsed': {'info': {'authority': '7QXGLRjvyFAmxdRaP9Wk18KwWTMfspF4Na2sr3o3PzxV', 'destination': 'BAxX7eMuSoxF8E4s1Xz5ukcLPHjvfC8Wv9JjSZci7tZ7', 'mint': 'Faf89929Ni9fbg4gmVZTca7eW6NFg877Jqn6MizT3Gvw', 'source': '8Ggb8th8pZv7eJbRmwkoyrDbSvwLPvMQN7QuHwjm113z', 'tokenAmount': {'amount': '200000000000', 'decimals': 8, 'uiAmount': 2000.0, 'uiAmountString': '2000' + } + }, 'type': 'transferChecked' + }, 'program': 'spl-token', 'programId': 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', 'stackHeight': 3 + }, + {'parsed': {'info': {'authority': '7LVHkVPVosXoWgH1PykWa5PjsfnyBviCdxC8ZUHJDy5U', 'destination': 'icV5K6iC8J7yyeP9YnJLH2jPRJYT7dBmzm844STxHkP', 'mint': 'So11111111111111111111111111111111111111112', 'source': 'Gvjgjv63zcQxLbcG2iYF3vcJ2nudRLEffN6hoo8p6Ewy', 'tokenAmount': {'amount': '32677742', 'decimals': 9, 'uiAmount': 0.032677742, 'uiAmountString': '0.032677742' + } + }, 'type': 'transferChecked' + }, 'program': 'spl-token', 'programId': 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', 'stackHeight': 3 + } + ] + } + ], 'logMessages': ['Program ComputeBudget111111111111111111111111111111 invoke [ + 1 + ]', 'Program ComputeBudget111111111111111111111111111111 success', 'Program ComputeBudget111111111111111111111111111111 invoke [ + 1 + ]', 'Program ComputeBudget111111111111111111111111111111 success', 'Program 11111111111111111111111111111111 invoke [ + 1 + ]', 'Program 11111111111111111111111111111111 success', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ + 1 + ]', 'Program log: Instruction: InitializeAccount', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 3443 of 216550 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program 6m2CDdhRgxpH4WjvdzxAYbGxwdGUz5MziiL5jek2kBma invoke [ + 1 + ]', 'Program log: Instruction: Swap2', 'Program log: order_id: 13978787684884160', 'Program log: Faf89929Ni9fbg4gmVZTca7eW6NFg877Jqn6MizT3Gvw', 'Program log: So11111111111111111111111111111111111111112', 'Program log: before_source_balance: 4000000000000, before_destination_balance: 0, amount_in: 200000000000, expect_amount_out: 32677742, min_return: 32350965', 'Program log: Dex: :RaydiumClmmSwapV2 amount_in: 200000000000, offset: 0', 'Program CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK invoke [ + 2 + ]', 'Program log: Instruction: SwapV2', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ + 3 + ]', 'Program log: Instruction: TransferChecked', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 6147 of 124210 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ + 3 + ]', 'Program log: Instruction: TransferChecked', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 6238 of 114194 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', 'Program data: QMbN6CYIceJeJCSmnTHyq+UhngUSm2ZraA736ls33e54BtTmtKTck18ss0Ijtep7x/QSQpelS9R/BLJJV8nYbkzHEgl6DWzmCqjvYwJou4+wNqo6UT4zu+LS3la0rmchxYO/qAt1QCxsBkiWENlC+V3iQ+9YxPA6ubSXncmJFhG2+Ianct8/LW6f8gEAAAAAAAAAAAAAAAAA0O2QLgAAAAAAAAAAAAAAAKdp93ci1pDdTQAAAAAAAAC1GvfaoAMAAAAAAAAAAAAAQFQBAA==', 'Program CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK consumed 77136 of 178600 compute units', 'Program CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK success', 'Program data: QMbN6CYIceILANDtkC4AAABun/IBAAAAAA==', 'Program log: SwapEvent { dex: RaydiumClmmSwapV2, amount_in: 200000000000, amount_out: 32677742 + }', 'Program log: 8Ggb8th8pZv7eJbRmwkoyrDbSvwLPvMQN7QuHwjm113z', 'Program log: icV5K6iC8J7yyeP9YnJLH2jPRJYT7dBmzm844STxHkP', 'Program log: after_source_balance: 3800000000000, after_destination_balance: 32677742', 'Program log: source_token_change: 200000000000, destination_token_change: 32677742', 'Program 6m2CDdhRgxpH4WjvdzxAYbGxwdGUz5MziiL5jek2kBma consumed 121291 of 213107 compute units', 'Program 6m2CDdhRgxpH4WjvdzxAYbGxwdGUz5MziiL5jek2kBma success', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [ + 1 + ]', 'Program log: Instruction: CloseAccount', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 2915 of 91816 compute units', 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success' + ], 'postBalances': [ + 93712473, + 0, + 2039280, + 72161280, + 72161280, + 1, + 255325042163, + 934087680, + 1, + 1141440, + 1705200, + 32092560, + 13641600, + 11637120, + 9935565889, + 2039280, + 1009200, + 1141440, + 521498880, + 617888496398, + 1141440 + ], 'postTokenBalances': [ + {'accountIndex': 2, 'mint': 'Faf89929Ni9fbg4gmVZTca7eW6NFg877Jqn6MizT3Gvw', 'owner': '7QXGLRjvyFAmxdRaP9Wk18KwWTMfspF4Na2sr3o3PzxV', 'programId': 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', 'uiTokenAmount': {'amount': '3800000000000', 'decimals': 8, 'uiAmount': 38000.0, 'uiAmountString': '38000' + } + }, + {'accountIndex': 14, 'mint': 'So11111111111111111111111111111111111111112', 'owner': '7LVHkVPVosXoWgH1PykWa5PjsfnyBviCdxC8ZUHJDy5U', 'programId': 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', 'uiTokenAmount': {'amount': '9933526609', 'decimals': 9, 'uiAmount': 9.933526609, 'uiAmountString': '9.933526609' + } + }, + {'accountIndex': 15, 'mint': 'Faf89929Ni9fbg4gmVZTca7eW6NFg877Jqn6MizT3Gvw', 'owner': '7LVHkVPVosXoWgH1PykWa5PjsfnyBviCdxC8ZUHJDy5U', 'programId': 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', 'uiTokenAmount': {'amount': '2460142552127709', 'decimals': 8, 'uiAmount': 24601425.52127709, 'uiAmountString': '24601425.52127709' + } + } + ], 'preBalances': [ + 61131858, + 0, + 2039280, + 72161280, + 72161280, + 1, + 255325042163, + 934087680, + 1, + 1141440, + 1705200, + 32092560, + 13641600, + 11637120, + 9968243631, + 2039280, + 1009200, + 1141440, + 521498880, + 617888496398, + 1141440 + ], 'preTokenBalances': [ + {'accountIndex': 2, 'mint': 'Faf89929Ni9fbg4gmVZTca7eW6NFg877Jqn6MizT3Gvw', 'owner': '7QXGLRjvyFAmxdRaP9Wk18KwWTMfspF4Na2sr3o3PzxV', 'programId': 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', 'uiTokenAmount': {'amount': '4000000000000', 'decimals': 8, 'uiAmount': 40000.0, 'uiAmountString': '40000' + } + }, + {'accountIndex': 14, 'mint': 'So11111111111111111111111111111111111111112', 'owner': '7LVHkVPVosXoWgH1PykWa5PjsfnyBviCdxC8ZUHJDy5U', 'programId': 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', 'uiTokenAmount': {'amount': '9966204351', 'decimals': 9, 'uiAmount': 9.966204351, 'uiAmountString': '9.966204351' + } + }, + {'accountIndex': 15, 'mint': 'Faf89929Ni9fbg4gmVZTca7eW6NFg877Jqn6MizT3Gvw', 'owner': '7LVHkVPVosXoWgH1PykWa5PjsfnyBviCdxC8ZUHJDy5U', 'programId': 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', 'uiTokenAmount': {'amount': '2459942552127709', 'decimals': 8, 'uiAmount': 24599425.52127709, 'uiAmountString': '24599425.52127709' + } + } + ], 'rewards': [], 'status': {'Ok': None + } + }, 'slot': 293628136, 'transaction': {'message': {'accountKeys': [ + {'pubkey': '7QXGLRjvyFAmxdRaP9Wk18KwWTMfspF4Na2sr3o3PzxV', 'signer': True, 'source': 'transaction', 'writable': True + }, + {'pubkey': 'icV5K6iC8J7yyeP9YnJLH2jPRJYT7dBmzm844STxHkP', 'signer': False, 'source': 'transaction', 'writable': True + }, + {'pubkey': '8Ggb8th8pZv7eJbRmwkoyrDbSvwLPvMQN7QuHwjm113z', 'signer': False, 'source': 'transaction', 'writable': True + }, + {'pubkey': 'DeRo9XMsizey3KWBWS3LHomFC7ZDny1NkFMZUEHgyX8a', 'signer': False, 'source': 'transaction', 'writable': True + }, + {'pubkey': '423toqoYT7obTRx8qmwVAeYZfMFRxkwwDzRAwJBd86K3', 'signer': False, 'source': 'transaction', 'writable': True + }, + {'pubkey': '11111111111111111111111111111111', 'signer': False, 'source': 'transaction', 'writable': False + }, + {'pubkey': 'Faf89929Ni9fbg4gmVZTca7eW6NFg877Jqn6MizT3Gvw', 'signer': False, 'source': 'transaction', 'writable': False + }, + {'pubkey': 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', 'signer': False, 'source': 'transaction', 'writable': False + }, + {'pubkey': 'ComputeBudget111111111111111111111111111111', 'signer': False, 'source': 'transaction', 'writable': False + }, + {'pubkey': '6m2CDdhRgxpH4WjvdzxAYbGxwdGUz5MziiL5jek2kBma', 'signer': False, 'source': 'transaction', 'writable': False + }, + {'pubkey': 'A1BBtTYJd4i3xU8D6Tc2FzU6ZN4oXZWXKZnCxwbHXr8x', 'signer': False, 'source': 'lookupTable', 'writable': True + }, + {'pubkey': 'HtsJ5S6K4NM2vXaWZ8k49JAggBgPGrryJ21ezdPBoUC6', 'signer': False, 'source': 'lookupTable', 'writable': True + }, + {'pubkey': 'E6ef1fYRvZ8ejtbdwaK1CDNAgpJxkWZ2pQsL4jaShjNU', 'signer': False, 'source': 'lookupTable', 'writable': True + }, + {'pubkey': '7LVHkVPVosXoWgH1PykWa5PjsfnyBviCdxC8ZUHJDy5U', 'signer': False, 'source': 'lookupTable', 'writable': True + }, + {'pubkey': 'Gvjgjv63zcQxLbcG2iYF3vcJ2nudRLEffN6hoo8p6Ewy', 'signer': False, 'source': 'lookupTable', 'writable': True + }, + {'pubkey': 'BAxX7eMuSoxF8E4s1Xz5ukcLPHjvfC8Wv9JjSZci7tZ7', 'signer': False, 'source': 'lookupTable', 'writable': True + }, + {'pubkey': 'SysvarRent111111111111111111111111111111111', 'signer': False, 'source': 'lookupTable', 'writable': False + }, + {'pubkey': 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb', 'signer': False, 'source': 'lookupTable', 'writable': False + }, + {'pubkey': 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr', 'signer': False, 'source': 'lookupTable', 'writable': False + }, + {'pubkey': 'So11111111111111111111111111111111111111112', 'signer': False, 'source': 'lookupTable', 'writable': False + }, + {'pubkey': 'CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK', 'signer': False, 'source': 'lookupTable', 'writable': False + } + ], 'addressTableLookups': [ + {'accountKey': '6ggEfUA8Y2BPkrg6yP2yjjEZRhnF6TyBG8q5tHQvdb2C', 'readonlyIndexes': [ + 1, + 4, + 5, + 41, + 13 + ], 'writableIndexes': [ + 27 + ] + }, + {'accountKey': '9kvMXWLiSBfbGxxrripJP4qZzHatdSbadYCAbwTexN28', 'readonlyIndexes': [], 'writableIndexes': [ + 17, + 18, + 12, + 13, + 15 + ] + } + ], 'instructions': [ + {'accounts': [], 'data': 'JPb7uH', 'programId': 'ComputeBudget111111111111111111111111111111', 'stackHeight': None + }, + {'accounts': [], 'data': '3VbWNdv4R831', 'programId': 'ComputeBudget111111111111111111111111111111', 'stackHeight': None + }, + {'parsed': {'info': {'base': '7QXGLRjvyFAmxdRaP9Wk18KwWTMfspF4Na2sr3o3PzxV', 'lamports': 2039280, 'newAccount': 'icV5K6iC8J7yyeP9YnJLH2jPRJYT7dBmzm844STxHkP', 'owner': 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', 'seed': '1728035372645', 'source': '7QXGLRjvyFAmxdRaP9Wk18KwWTMfspF4Na2sr3o3PzxV', 'space': 165 + }, 'type': 'createAccountWithSeed' + }, 'program': 'system', 'programId': '11111111111111111111111111111111', 'stackHeight': None + }, + {'parsed': {'info': {'account': 'icV5K6iC8J7yyeP9YnJLH2jPRJYT7dBmzm844STxHkP', 'mint': 'So11111111111111111111111111111111111111112', 'owner': '7QXGLRjvyFAmxdRaP9Wk18KwWTMfspF4Na2sr3o3PzxV', 'rentSysvar': 'SysvarRent111111111111111111111111111111111' + }, 'type': 'initializeAccount' + }, 'program': 'spl-token', 'programId': 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', 'stackHeight': None + }, + {'accounts': ['7QXGLRjvyFAmxdRaP9Wk18KwWTMfspF4Na2sr3o3PzxV', '8Ggb8th8pZv7eJbRmwkoyrDbSvwLPvMQN7QuHwjm113z', 'icV5K6iC8J7yyeP9YnJLH2jPRJYT7dBmzm844STxHkP', 'Faf89929Ni9fbg4gmVZTca7eW6NFg877Jqn6MizT3Gvw', 'So11111111111111111111111111111111111111112', 'CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK', '7QXGLRjvyFAmxdRaP9Wk18KwWTMfspF4Na2sr3o3PzxV', '8Ggb8th8pZv7eJbRmwkoyrDbSvwLPvMQN7QuHwjm113z', 'icV5K6iC8J7yyeP9YnJLH2jPRJYT7dBmzm844STxHkP', 'A1BBtTYJd4i3xU8D6Tc2FzU6ZN4oXZWXKZnCxwbHXr8x', '7LVHkVPVosXoWgH1PykWa5PjsfnyBviCdxC8ZUHJDy5U', 'BAxX7eMuSoxF8E4s1Xz5ukcLPHjvfC8Wv9JjSZci7tZ7', 'Gvjgjv63zcQxLbcG2iYF3vcJ2nudRLEffN6hoo8p6Ewy', 'HtsJ5S6K4NM2vXaWZ8k49JAggBgPGrryJ21ezdPBoUC6', 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb', 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr', 'Faf89929Ni9fbg4gmVZTca7eW6NFg877Jqn6MizT3Gvw', 'So11111111111111111111111111111111111111112', 'DeRo9XMsizey3KWBWS3LHomFC7ZDny1NkFMZUEHgyX8a', 'E6ef1fYRvZ8ejtbdwaK1CDNAgpJxkWZ2pQsL4jaShjNU', '423toqoYT7obTRx8qmwVAeYZfMFRxkwwDzRAwJBd86K3', '11111111111111111111111111111111' + ], 'data': '3sRDYcTeC9Apf4dwtR12F5tp2Pc4P5tDNYNtKqVRGjUmVJ4pEyVYZdwph4vkfkgyChXffRuiYRUXchviqNRXfd5ok6ub9PQK', 'programId': '6m2CDdhRgxpH4WjvdzxAYbGxwdGUz5MziiL5jek2kBma', 'stackHeight': None + }, + {'parsed': {'info': {'account': 'icV5K6iC8J7yyeP9YnJLH2jPRJYT7dBmzm844STxHkP', 'destination': '7QXGLRjvyFAmxdRaP9Wk18KwWTMfspF4Na2sr3o3PzxV', 'owner': '7QXGLRjvyFAmxdRaP9Wk18KwWTMfspF4Na2sr3o3PzxV' + }, 'type': 'closeAccount' + }, 'program': 'spl-token', 'programId': 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', 'stackHeight': None + } + ], 'recentBlockhash': 'C2H99uuxKQRWeTngCRZoNHbQ2nDBAA2Uhk2USp7tDX9Z' + }, 'signatures': ['3PZLedLDQTb7ddor3XmwbQQycBCgFfMDXHWBRd9rK7gekWTHWetnEGEGpZb6XFep7EbMZFrJweM3q1tkGygZDthB' + ] + }, 'version': 0 +} \ No newline at end of file From 1eeaec6e843b003e9478ab6c7cdec04c27ab63bc Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Sat, 5 Oct 2024 19:04:36 +0300 Subject: [PATCH 04/21] try parsing swap events --- .env | 2 +- crypto/sol/app.py | 178 +++++++++++++++++++++-------- crypto/sol/transactionExample.json | 5 +- 3 files changed, 138 insertions(+), 47 deletions(-) diff --git a/.env b/.env index 044dfd2..1f066e6 100644 --- a/.env +++ b/.env @@ -28,4 +28,4 @@ OPENAI_API_KEY=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 # List models available from Groq # aider --models groq/ -SUBSCRIPTION_ID='1518430' +SUBSCRIPTION_ID='2217755' diff --git a/crypto/sol/app.py b/crypto/sol/app.py index 1d54feb..17747e2 100644 --- a/crypto/sol/app.py +++ b/crypto/sol/app.py @@ -19,6 +19,7 @@ from dotenv import load_dotenv,set_key import aiohttp from typing import List, Dict import requests +import threading load_dotenv() @@ -28,7 +29,10 @@ app = Flask(__name__) def get_latest_log_file(): log_dir = './logs' try: - files = [f for f in os.listdir(log_dir) if os.path.isfile(os.path.join(log_dir, f))] + # files = [f for f in os.listdir(log_dir) if os.path.isfile(os.path.join(log_dir, f))] + # filter files mask log_20241005_004103_143116.json + files = [f for f in os.listdir(log_dir) if os.path.isfile(os.path.join(log_dir, f)) and f.startswith('log_')] + latest_file = max(files, key=lambda x: os.path.getctime(os.path.join(log_dir, x))) return os.path.join(log_dir, latest_file) except Exception as e: @@ -242,7 +246,7 @@ async def get_token_balance(wallet_address, token_address): logging.debug(f"No account found for {token_address} in {wallet_address}") return 0 except Exception as e: - logging.error(f"Error getting balance for {token_address} in {wallet_address}: {str(e)}") + logging.error(f"Error getting balance for {token_address} in {wallet_address}: {str(e)} \r\n {e}") return 0 @@ -498,15 +502,6 @@ async def get_transaction_details_rpc(tx_signature): } ] } - # request = { - # "jsonrpc": "2.0", - # "id": 1, - # "method": "getConfirmedTransaction", - # "params": [ - # tx_signature, - # "json" - # ] - # } try: response = requests.post(url, headers=headers, data=json.dumps(data)) response.raise_for_status() # Raises an error for bad responses @@ -533,6 +528,35 @@ async def save_log(log): logging.error(f"Error saving RPC log: {e}") +def determine_token(pubkey, watched_tokens): + # Check if the pubkey matches any watched token addresses + for token, address in watched_tokens.items(): + if pubkey == address: + return token + return "Unknown" + +def parse_amount_from_logs(logs): + amount_in = 0 + amount_out = 0 + + for log in logs: + if 'SwapEvent' in log: + # Extract amounts from the log line + parts = log.split('amount_in: ')[1].split(', amount_out: ') + amount_in = int(parts[0]) + amount_out = int(parts[1].split(' ')[0]) + + return amount_in, amount_out + +def extract_swap_details(instruction, logs, watched_tokens): + # Extract source and target tokens along with amounts + from_pubkey = instruction['accounts'][0] + to_pubkey = instruction['accounts'][1] + amount_in, amount_out = parse_amount_from_logs(logs) + return from_pubkey, to_pubkey, amount_in, amount_out + + + async def process_log(log_result): if log_result['value']['err']: return @@ -541,45 +565,96 @@ async def process_log(log_result): logs = log_result['value']['logs'] try: - - try: - transaction = await get_transaction_details_rpc(tx_signature_str) - except Exception as e: - logging.error(f"Error fetching transaction details: {e}") - return - - - - # Convert the base58 signature string to bytes - tx_signature = Signature(b58decode(tx_signature_str)) - # Fetch transaction details - tx_result = await solana_client.get_transaction(tx_signature, max_supported_transaction_version=0) - #tx_result = await get_transaction_details(tx_signature_str) - if tx_result.value is None: - logging.error(f"Transaction not found: {tx_signature_str}") - return - transaction = tx_result.value.transaction + # Detect swap operations in logs + swap_operations = ['Program log: Instruction: Swap', 'Program log: Instruction: Swap2'] for log_entry in logs: - if 'Program log: Instruction: Swap' in log_entry: - for instruction in message.instructions: - if instruction.program_id == TOKEN_ADDRESSES['SOL']: - from_pubkey = instruction.accounts[0] - to_pubkey = instruction.accounts[1] - amount = int(instruction.data, 16) / 1e9 + if any(op in log_entry for op in swap_operations): + try: + # ++ OLD using solana-py + # # Convert the base58 signature string to bytes + # tx_signature = Signature(b58decode(tx_signature_str)) + # # Fetch transaction details + # tx_result = await solana_client.get_transaction(tx_signature, max_supported_transaction_version=0) + # #tx_result = await get_transaction_details(tx_signature_str) + # if tx_result.value is None: + # logging.error(f"Transaction not found: {tx_signature_str}") + # return + # transaction2 = tx_result.value.transaction + # -- OLD using solana-py + + watched_tokens = await get_non_zero_token_balances(FOLLOWED_WALLET) + details = parse_swap_logs(logs) + transaction = await get_transaction_details_rpc(tx_signature_str) - if from_pubkey == FOLLOWED_WALLET: + instructions = transaction['transaction']['message']['instructions'] + + for instruction in instructions: + from_pubkey, to_pubkey, amount_in, amount_out = extract_swap_details(instruction, logs, watched_tokens) + + if from_pubkey in watched_tokens.values() or to_pubkey in watched_tokens.values(): + from_token = determine_token(from_pubkey, watched_tokens) + to_token = determine_token(to_pubkey, watched_tokens) + move = { - 'token': 'SOL', - 'amount': amount, - 'to_token': 'Unknown' + 'token': from_token, + 'amount': amount_in, + 'to_token': to_token } - message_text = f"Swap detected:\nFrom: {from_pubkey}\nTo: {to_pubkey}\nAmount: {amount} SOL" + message_text = ( + f"Swap detected:\n" + f"From: {from_pubkey} ({from_token})\n" + f"To: {to_pubkey} ({to_token})\n" + f"Amount In: {amount_in}\n" + f"Amount Out: {amount_out}" + ) await send_telegram_message(message_text) await follow_move(move) + + + except Exception as e: + logging.error(f"Error fetching transaction details: {e}") + return + except Exception as e: logging.error(f"Error processing log: {e}") +def parse_swap_logs(logs): + swap_details = { + "source_token_address": "", + "destination_token_address": "", + "amount_in": 0, + "amount_out": 0 + } + + for log in logs: + if "SwapEvent" in log: + # Extract amounts from SwapEvent + parts = log.split("amount_in: ")[1].split(", amount_out: ") + swap_details["amount_in"] = int(parts[0]) + swap_details["amount_out"] = int(parts[1].split(" ")[0]) + + if "source_token_change:" in log: + # Extract source and destination token changes + changes = log.split(", ") + for change in changes: + key, value = change.split(": ") + if key == "source_token_change": + swap_details["amount_in"] = int(value) + elif key == "destination_token_change": + swap_details["amount_out"] = int(value) + + if "Program log:" in log and len(log.split()) == 2: + # Extract token addresses (assuming they are logged as single entries) + token_address = log.split(": ")[1] + if not swap_details["source_token_address"]: + swap_details["source_token_address"] = token_address + elif not swap_details["destination_token_address"]: + swap_details["destination_token_address"] = token_address + + return swap_details + + async def on_logs(log): logging.debug(f"Received log: {log}") await save_log(log) @@ -662,15 +737,28 @@ async def subscribe_to_wallet(): logger = logging.getLogger(__name__) + async def main(): # Initialize logging logging.basicConfig(level=logging.DEBUG) - # logging.basicConfig(level=logging.INFO) - await send_telegram_message("Solana Agent Started. Connecting to mainnet...") - #await subscribe_to_wallet() + await subscribe_to_wallet() + +def run_flask(): + # Run Flask app without the reloader, so we can run the async main function + app.run(debug=False, port=3001, use_reloader=False) if __name__ == '__main__': - - app.run(debug=True,port=3001) + # Start Flask in a separate thread + flask_thread = threading.Thread(target=run_flask) + flask_thread.start() + + # Create an event loop for the async tasks + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) + # Start Flask in a separate thread + flask_thread = threading.Thread(target=run_flask) + flask_thread.start() + + # Run the async main function asyncio.run(main()) diff --git a/crypto/sol/transactionExample.json b/crypto/sol/transactionExample.json index 525ad4c..fbbc586 100644 --- a/crypto/sol/transactionExample.json +++ b/crypto/sol/transactionExample.json @@ -1,4 +1,7 @@ -{'blockTime': 1728035375, 'meta': {'computeUnitsConsumed': 128099, 'err': None, 'fee': 97127, 'innerInstructions': [ +{ + 'blockTime': 1728035375, +'meta': {'computeUnitsConsumed': 128099, +'err': None, 'fee': 97127, 'innerInstructions': [ {'index': 4, 'instructions': [ {'accounts': ['7QXGLRjvyFAmxdRaP9Wk18KwWTMfspF4Na2sr3o3PzxV', 'A1BBtTYJd4i3xU8D6Tc2FzU6ZN4oXZWXKZnCxwbHXr8x', '7LVHkVPVosXoWgH1PykWa5PjsfnyBviCdxC8ZUHJDy5U', '8Ggb8th8pZv7eJbRmwkoyrDbSvwLPvMQN7QuHwjm113z', 'icV5K6iC8J7yyeP9YnJLH2jPRJYT7dBmzm844STxHkP', 'BAxX7eMuSoxF8E4s1Xz5ukcLPHjvfC8Wv9JjSZci7tZ7', 'Gvjgjv63zcQxLbcG2iYF3vcJ2nudRLEffN6hoo8p6Ewy', 'HtsJ5S6K4NM2vXaWZ8k49JAggBgPGrryJ21ezdPBoUC6', 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb', 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr', 'Faf89929Ni9fbg4gmVZTca7eW6NFg877Jqn6MizT3Gvw', 'So11111111111111111111111111111111111111112', 'DeRo9XMsizey3KWBWS3LHomFC7ZDny1NkFMZUEHgyX8a', 'E6ef1fYRvZ8ejtbdwaK1CDNAgpJxkWZ2pQsL4jaShjNU', '423toqoYT7obTRx8qmwVAeYZfMFRxkwwDzRAwJBd86K3' ], 'data': 'ASCsAbe1UnDmvdhmjnwFdPMzcfkrayiNaFZ61j7FiXYFR5MbydQDnzNL', 'programId': 'CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK', 'stackHeight': 2 From 42c290fbc61d5f850c74c7f2219009a5d1d5204c Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Sat, 5 Oct 2024 19:15:00 +0300 Subject: [PATCH 05/21] parse transaction wip --- crypto/sol/app.py | 56 +++++++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/crypto/sol/app.py b/crypto/sol/app.py index 17747e2..f5ca76f 100644 --- a/crypto/sol/app.py +++ b/crypto/sol/app.py @@ -619,41 +619,49 @@ async def process_log(log_result): except Exception as e: logging.error(f"Error processing log: {e}") + def parse_swap_logs(logs): - swap_details = { - "source_token_address": "", - "destination_token_address": "", - "amount_in": 0, - "amount_out": 0 - } - + total_amount_in = 0 + total_amount_out = 0 + source_token_address = "" + destination_token_address = "" + for log in logs: if "SwapEvent" in log: - # Extract amounts from SwapEvent - parts = log.split("amount_in: ")[1].split(", amount_out: ") - swap_details["amount_in"] = int(parts[0]) - swap_details["amount_out"] = int(parts[1].split(" ")[0]) - + parts = log.split("{ ")[1].strip(" }").split(", ") + event_details = {} + for part in parts: + key, value = part.split(": ") + event_details[key.strip()] = value.strip() + + # Aggregate amounts + total_amount_in += int(event_details.get("amount_in", 0)) + total_amount_out += int(event_details.get("amount_out", 0)) + if "source_token_change:" in log: - # Extract source and destination token changes + # Extract final source and destination token addresses changes = log.split(", ") - for change in changes: + for change in changes.Trim('Program log:'): key, value = change.split(": ") if key == "source_token_change": - swap_details["amount_in"] = int(value) + total_amount_in = int(value) elif key == "destination_token_change": - swap_details["amount_out"] = int(value) - + total_amount_out = int(value) + if "Program log:" in log and len(log.split()) == 2: - # Extract token addresses (assuming they are logged as single entries) + # Extract token addresses token_address = log.split(": ")[1] - if not swap_details["source_token_address"]: - swap_details["source_token_address"] = token_address - elif not swap_details["destination_token_address"]: - swap_details["destination_token_address"] = token_address - - return swap_details + if not source_token_address: + source_token_address = token_address + elif not destination_token_address: + destination_token_address = token_address + return { + "source_token_address": source_token_address, + "destination_token_address": destination_token_address, + "total_amount_in": total_amount_in, + "total_amount_out": total_amount_out, + } async def on_logs(log): logging.debug(f"Received log: {log}") From b5bc6505a270d5aeff4d00f4b45efe6e98e92932 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Sat, 5 Oct 2024 20:36:25 +0300 Subject: [PATCH 06/21] transaction parse progress --- crypto/sol/app.py | 126 +++++++++++++++++++++++++++++----------------- 1 file changed, 80 insertions(+), 46 deletions(-) diff --git a/crypto/sol/app.py b/crypto/sol/app.py index f5ca76f..c8cfb1f 100644 --- a/crypto/sol/app.py +++ b/crypto/sol/app.py @@ -253,7 +253,8 @@ async def get_token_balance(wallet_address, token_address): ENV_FILE = '.env' async def save_subscription_id(subscription_id): - set_key(ENV_FILE, "SUBSCRIPTION_ID", str(subscription_id)) + # storing subscription id in .env file disabled + #set_key(ENV_FILE, "SUBSCRIPTION_ID", str(subscription_id)) logger.info(f"Saved subscription ID: {subscription_id}") async def load_subscription_id(): @@ -486,7 +487,7 @@ from solders.pubkey import Pubkey from solders.transaction import Transaction from solders.signature import Signature -async def get_transaction_details_rpc(tx_signature): +async def get_transaction_details_rpc(tx_signature, readfromDump=False): url = SOLANA_HTTP_URL # url = 'https://solana.drpc.org' headers = {"Content-Type": "application/json"} @@ -503,9 +504,17 @@ async def get_transaction_details_rpc(tx_signature): ] } try: - response = requests.post(url, headers=headers, data=json.dumps(data)) - response.raise_for_status() # Raises an error for bad responses - transaction_details = response.json() + if readfromDump and os.path.exists('./logs/transation_details.json'): + with open('./logs/transation_details.json', 'r') as f: # trump_swap_tr_details + transaction_details = json.load(f) + return transaction_details + else: + response = requests.post(url, headers=headers, data=json.dumps(data)) + response.raise_for_status() # Raises an error for bad responses + transaction_details = response.json() + with open('./logs/transation_details.json', 'w') as f: + json.dump(transaction_details, f, indent=2) + if 'result' in transaction_details: print(transaction_details['result']) @@ -584,33 +593,67 @@ async def process_log(log_result): # -- OLD using solana-py watched_tokens = await get_non_zero_token_balances(FOLLOWED_WALLET) - details = parse_swap_logs(logs) - transaction = await get_transaction_details_rpc(tx_signature_str) + details = parse_swap_logs(logs, watched_tokens) + transaction = await get_transaction_details_rpc(tx_signature_str, True) - instructions = transaction['transaction']['message']['instructions'] + # instructions = transaction['transaction']['message']['instructions'] - for instruction in instructions: - from_pubkey, to_pubkey, amount_in, amount_out = extract_swap_details(instruction, logs, watched_tokens) + # for instruction in instructions: + # from_pubkey, to_pubkey, amount_in, amount_out = extract_swap_details(instruction, logs, watched_tokens) - if from_pubkey in watched_tokens.values() or to_pubkey in watched_tokens.values(): - from_token = determine_token(from_pubkey, watched_tokens) - to_token = determine_token(to_pubkey, watched_tokens) + # if from_pubkey in watched_tokens.values() or to_pubkey in watched_tokens.values(): + # from_token = determine_token(from_pubkey, watched_tokens) + # to_token = determine_token(to_pubkey, watched_tokens) - move = { - 'token': from_token, - 'amount': amount_in, - 'to_token': to_token - } - message_text = ( - f"Swap detected:\n" - f"From: {from_pubkey} ({from_token})\n" - f"To: {to_pubkey} ({to_token})\n" - f"Amount In: {amount_in}\n" - f"Amount Out: {amount_out}" - ) - await send_telegram_message(message_text) - await follow_move(move) - + # move = { + # 'token': from_token, + # 'amount': amount_in, + # 'to_token': to_token + # } + # message_text = ( + # f"Swap detected:\n" + # f"From: {from_pubkey} ({from_token})\n" + # f"To: {to_pubkey} ({to_token})\n" + # f"Amount In: {amount_in}\n" + # f"Amount Out: {amount_out}" + # ) + # await send_telegram_message(message_text) + # await follow_move(move) + + tokens = [] + + # Check inner instructions for transfers and mints + for instruction_set in transaction.get('meta', {}).get('innerInstructions', []): + for instruction in instruction_set.get('instructions', []): + if 'parsed' in instruction and 'info' in instruction['parsed']: + info = instruction['parsed']['info'] + if 'amount' in info: + amount = info['amount'] + # Assume mint is available for mintTo + mint = info.get('mint', 'Unknown') + tokens.append({'amount': amount, 'mint': mint}) + + # Check post token balances for final token states + for balance in transaction.get('postTokenBalances', []): + amount = balance['uiTokenAmount']['amount'] + mint = balance['mint'] + tokens.append({'amount': amount, 'mint': mint}) + + # get amount_in, amount_out and token in and token out and USD value + swap_details = { + 'amount_in': details['total_amount_in'], + 'amount_out': details['total_amount_out'], + 'tokens': tokens + } + + message_text = ( + f"Swap detected:\n" + f"Amount In: {swap_details['amount_in']}\n" + f"Amount Out: {swap_details['amount_out']}" + ) + + await send_telegram_message(message_text) + # await follow_move(move) except Exception as e: logging.error(f"Error fetching transaction details: {e}") @@ -619,12 +662,10 @@ async def process_log(log_result): except Exception as e: logging.error(f"Error processing log: {e}") - -def parse_swap_logs(logs): +def parse_swap_logs(logs, watched_tokens): total_amount_in = 0 total_amount_out = 0 - source_token_address = "" - destination_token_address = "" + token_addresses = [] for log in logs: if "SwapEvent" in log: @@ -639,26 +680,19 @@ def parse_swap_logs(logs): total_amount_out += int(event_details.get("amount_out", 0)) if "source_token_change:" in log: - # Extract final source and destination token addresses + # Extract final source and destination token changes changes = log.split(", ") - for change in changes.Trim('Program log:'): - key, value = change.split(": ") + for change in changes: + key_value = change.split(": ", 1) + if len(key_value) != 2: + continue + key, value = key_value if key == "source_token_change": total_amount_in = int(value) elif key == "destination_token_change": total_amount_out = int(value) - if "Program log:" in log and len(log.split()) == 2: - # Extract token addresses - token_address = log.split(": ")[1] - if not source_token_address: - source_token_address = token_address - elif not destination_token_address: - destination_token_address = token_address - return { - "source_token_address": source_token_address, - "destination_token_address": destination_token_address, "total_amount_in": total_amount_in, "total_amount_out": total_amount_out, } @@ -750,7 +784,7 @@ async def main(): # Initialize logging logging.basicConfig(level=logging.DEBUG) await send_telegram_message("Solana Agent Started. Connecting to mainnet...") - await subscribe_to_wallet() + # await subscribe_to_wallet() def run_flask(): # Run Flask app without the reloader, so we can run the async main function From 019e9f66d40a4213492295ee87e74ef241283ede Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Sat, 5 Oct 2024 21:29:45 +0300 Subject: [PATCH 07/21] tick --- crypto/sol/app.py | 158 ++++++++++++++++++++++++++++++---------------- 1 file changed, 102 insertions(+), 56 deletions(-) diff --git a/crypto/sol/app.py b/crypto/sol/app.py index c8cfb1f..d1e0894 100644 --- a/crypto/sol/app.py +++ b/crypto/sol/app.py @@ -8,6 +8,11 @@ from solana.rpc.websocket_api import connect from solana.rpc.types import TokenAccountOpts, TxOpts from solana.rpc.commitment import Confirmed from solders.pubkey import Pubkey +from solders.keypair import Keypair +from solders.transaction import Transaction +from solders.message import Message +from solders.instruction import Instruction +from solders.hash import Hash from dexscreener import DexscreenerClient from telegram import Bot from telegram.constants import ParseMode @@ -83,7 +88,7 @@ bot = Bot(token=TELEGRAM_BOT_TOKEN) TOKEN_ADDRESSES = { "SOL": "So11111111111111111111111111111111111111112", "USDC": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", - "TARD": "4nfn86ssbv7wiqcsw7bpvn46k24jhe334fudtyxhp1og" + "TARD": "4nfn86ssbv7wiqcsw7bpvn46k24jhe334fudtyxhp1og", } async def send_telegram_message(message): @@ -231,10 +236,20 @@ async def convert_balances_to_currency(balances, token_prices, sol_price): async def get_token_balance(wallet_address, token_address): try: - response = await solana_client.get_token_accounts_by_owner( + response = await solana_client.get_token_accounts_by_owner_json_parsed( Pubkey.from_string(wallet_address), - {"mint": Pubkey.from_string(token_address)} + opts=TokenAccountOpts( + mint=Pubkey.from_string(token_address), + # program_id=Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") + ), + commitment=Confirmed ) + + + # response = await solana_client.get_token_accounts_by_owner( + # Pubkey.from_string(wallet_address), + # {"mint": Pubkey.from_string(token_address)} + # ) if response['result']['value']: balance = await solana_client.get_token_account_balance( response['result']['value'][0]['pubkey'] @@ -250,6 +265,55 @@ async def get_token_balance(wallet_address, token_address): return 0 +async def get_token_balance_rpc(wallet_address, token_address): + url = SOLANA_HTTP_URL + headers = {"Content-Type": "application/json"} + data = { + "jsonrpc": "2.0", + "id": 1, + "method": "getTokenAccountsByOwner", + "params": [ + wallet_address, + { + "mint": token_address + }, + { + "encoding": "jsonParsed" + } + ] + } + try: + response = requests.post(url, headers=headers, data=json.dumps(data)) + response.raise_for_status() # Raises an error for bad responses + accounts = response.json() + + if 'result' in accounts and accounts['result']['value']: + first_account = accounts['result']['value'][0]['pubkey'] + balance_data = { + "jsonrpc": "2.0", + "id": 1, + "method": "getTokenAccountBalance", + "params": [ + first_account + ] + } + balance_response = requests.post(url, headers=headers, data=json.dumps(balance_data)) + balance_response.raise_for_status() + balance = balance_response.json() + + if 'result' in balance and 'value' in balance['result']: + amount = float(balance['result']['value']['uiAmount']) + logging.debug(f"Balance for {token_address} in {wallet_address}: {amount}") + return amount + else: + logging.debug(f"No balance found for {token_address} in {wallet_address}") + return 0 + else: + logging.debug(f"No account found for {token_address} in {wallet_address}") + return 0 + except requests.exceptions.RequestException as e: + logging.error(f"Error getting balance for {token_address} in {wallet_address}: {str(e)} \r\n {e}") + return 0 ENV_FILE = '.env' async def save_subscription_id(subscription_id): @@ -335,16 +399,24 @@ async def send_initial_wallet_states(followed_wallet, your_wallet): async def get_non_zero_token_balances(wallet_address): - non_zero_balances = {} + + followed_wallet_balances, followed_token_addresses = await get_wallet_balances(FOLLOWED_WALLET) + # return non zero balances for followed wallet + non_zero_balances = {token: address for token, address in {**followed_wallet_balances}.items() if address is not None and address > 0} logging.info(f"Getting non-zero balances for wallet: {wallet_address}") - for token, address in TOKEN_ADDRESSES.items(): - balance = await get_token_balance(wallet_address, address) - if balance > 0: - non_zero_balances[token] = address - logging.debug(f"Non-zero balance for {token}: {balance}") return non_zero_balances + # non_zero_balances = {} + # logging.info(f"Getting non-zero balances for wallet: {wallet_address}") + # for token, address in TOKEN_ADDRESSES.items(): + # balance = await get_token_balance_rpc(wallet_address, address) + # if balance > 0: + # non_zero_balances[token] = address + # logging.debug(f"Non-zero balance for {token}: {balance}") + # return non_zero_balances + + async def list_initial_wallet_states(): global TOKEN_ADDRESSES, FOLLOWED_WALLET_VALUE, YOUR_WALLET_VALUE @@ -579,67 +651,40 @@ async def process_log(log_result): for log_entry in logs: if any(op in log_entry for op in swap_operations): - try: - # ++ OLD using solana-py - # # Convert the base58 signature string to bytes - # tx_signature = Signature(b58decode(tx_signature_str)) - # # Fetch transaction details - # tx_result = await solana_client.get_transaction(tx_signature, max_supported_transaction_version=0) - # #tx_result = await get_transaction_details(tx_signature_str) - # if tx_result.value is None: - # logging.error(f"Transaction not found: {tx_signature_str}") - # return - # transaction2 = tx_result.value.transaction - # -- OLD using solana-py - + try: watched_tokens = await get_non_zero_token_balances(FOLLOWED_WALLET) details = parse_swap_logs(logs, watched_tokens) transaction = await get_transaction_details_rpc(tx_signature_str, True) - - # instructions = transaction['transaction']['message']['instructions'] - - # for instruction in instructions: - # from_pubkey, to_pubkey, amount_in, amount_out = extract_swap_details(instruction, logs, watched_tokens) - - # if from_pubkey in watched_tokens.values() or to_pubkey in watched_tokens.values(): - # from_token = determine_token(from_pubkey, watched_tokens) - # to_token = determine_token(to_pubkey, watched_tokens) - - # move = { - # 'token': from_token, - # 'amount': amount_in, - # 'to_token': to_token - # } - # message_text = ( - # f"Swap detected:\n" - # f"From: {from_pubkey} ({from_token})\n" - # f"To: {to_pubkey} ({to_token})\n" - # f"Amount In: {amount_in}\n" - # f"Amount Out: {amount_out}" - # ) - # await send_telegram_message(message_text) - # await follow_move(move) - - tokens = [] - + + tokens = [] # Check inner instructions for transfers and mints for instruction_set in transaction.get('meta', {}).get('innerInstructions', []): for instruction in instruction_set.get('instructions', []): if 'parsed' in instruction and 'info' in instruction['parsed']: info = instruction['parsed']['info'] + amount = None + mint = 'Unknown' + + # Check for amount in transfer and transferChecked instructions if 'amount' in info: amount = info['amount'] - # Assume mint is available for mintTo - mint = info.get('mint', 'Unknown') + elif 'tokenAmount' in info and 'amount' in info['tokenAmount']: + amount = info['tokenAmount']['amount'] + + # Get mint if available + if 'mint' in info: + mint = info['mint'] + + if amount is not None: tokens.append({'amount': amount, 'mint': mint}) # Check post token balances for final token states for balance in transaction.get('postTokenBalances', []): amount = balance['uiTokenAmount']['amount'] mint = balance['mint'] - tokens.append({'amount': amount, 'mint': mint}) + tokens.append({'amount': amount, 'mint': mint}) - # get amount_in, amount_out and token in and token out and USD value + # Get amount_in, amount_out, tokens, and USD value swap_details = { 'amount_in': details['total_amount_in'], 'amount_out': details['total_amount_out'], @@ -649,11 +694,12 @@ async def process_log(log_result): message_text = ( f"Swap detected:\n" f"Amount In: {swap_details['amount_in']}\n" - f"Amount Out: {swap_details['amount_out']}" + f"Amount Out: {swap_details['amount_out']}\n" + f"Tokens: {tokens}" ) await send_telegram_message(message_text) - # await follow_move(move) + await follow_move(swap_details) except Exception as e: logging.error(f"Error fetching transaction details: {e}") @@ -784,7 +830,7 @@ async def main(): # Initialize logging logging.basicConfig(level=logging.DEBUG) await send_telegram_message("Solana Agent Started. Connecting to mainnet...") - # await subscribe_to_wallet() + await subscribe_to_wallet() def run_flask(): # Run Flask app without the reloader, so we can run the async main function From 52c4877ed4882aa70d17aa9444b6945e68a2897e Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Sat, 5 Oct 2024 21:49:32 +0300 Subject: [PATCH 08/21] parsing logs only - no details --- crypto/sol/app.py | 158 ++++++++++++++++++++++++++++------------------ 1 file changed, 96 insertions(+), 62 deletions(-) diff --git a/crypto/sol/app.py b/crypto/sol/app.py index d1e0894..91fac0e 100644 --- a/crypto/sol/app.py +++ b/crypto/sol/app.py @@ -644,57 +644,71 @@ async def process_log(log_result): tx_signature_str = log_result['value']['signature'] logs = log_result['value']['logs'] - try: # Detect swap operations in logs swap_operations = ['Program log: Instruction: Swap', 'Program log: Instruction: Swap2'] for log_entry in logs: if any(op in log_entry for op in swap_operations): - try: - watched_tokens = await get_non_zero_token_balances(FOLLOWED_WALLET) - details = parse_swap_logs(logs, watched_tokens) - transaction = await get_transaction_details_rpc(tx_signature_str, True) - - tokens = [] - # Check inner instructions for transfers and mints - for instruction_set in transaction.get('meta', {}).get('innerInstructions', []): - for instruction in instruction_set.get('instructions', []): - if 'parsed' in instruction and 'info' in instruction['parsed']: - info = instruction['parsed']['info'] - amount = None - mint = 'Unknown' - - # Check for amount in transfer and transferChecked instructions - if 'amount' in info: - amount = info['amount'] - elif 'tokenAmount' in info and 'amount' in info['tokenAmount']: - amount = info['tokenAmount']['amount'] - - # Get mint if available - if 'mint' in info: - mint = info['mint'] - - if amount is not None: - tokens.append({'amount': amount, 'mint': mint}) + try: - # Check post token balances for final token states - for balance in transaction.get('postTokenBalances', []): - amount = balance['uiTokenAmount']['amount'] - mint = balance['mint'] - tokens.append({'amount': amount, 'mint': mint}) + + watched_tokens = await get_non_zero_token_balances(FOLLOWED_WALLET) + details = parse_swap_logs(logs) + # transaction = await get_transaction_details_rpc(tx_signature_str, True) + + # tokens = [] + # source_token = None + # target_token = None + + # # Check inner instructions for transfers and mints + # for instruction_set in transaction.get('meta', {}).get('innerInstructions', []): + # for instruction in instruction_set.get('instructions', []): + # if 'parsed' in instruction and 'info' in instruction['parsed']: + # info = instruction['parsed']['info'] + # amount = None + # mint = 'Unknown' + + # # Check for amount in transfer and transferChecked instructions + # if 'amount' in info: + # amount = info['amount'] + # elif 'tokenAmount' in info and 'amount' in info['tokenAmount']: + # amount = info['tokenAmount']['amount'] + + # # Get mint if available + # if 'mint' in info: + # mint = info['mint'] + + # if amount is not None: + # tokens.append({'amount': amount, 'mint': mint}) + + # # Identify source and target tokens + # if 'source' in info: + # source_token = info['source'] + # if 'destination' in info: + # target_token = info['destination'] + + # # Check post token balances for final token states + # for balance in transaction.get('postTokenBalances', []): + # amount = balance['uiTokenAmount']['amount'] + # mint = balance['mint'] + # tokens.append({'amount': amount, 'mint': mint}) # Get amount_in, amount_out, tokens, and USD value swap_details = { 'amount_in': details['total_amount_in'], 'amount_out': details['total_amount_out'], - 'tokens': tokens + 'tokens': tokens, + 'source_token': source_token, + 'target_token': target_token } message_text = ( f"Swap detected:\n" f"Amount In: {swap_details['amount_in']}\n" f"Amount Out: {swap_details['amount_out']}\n" + f"Source Token: {swap_details['source_token']}\n" + f"Target Token: {swap_details['target_token']}\n" f"Tokens: {tokens}" ) @@ -704,43 +718,63 @@ async def process_log(log_result): except Exception as e: logging.error(f"Error fetching transaction details: {e}") return - + except Exception as e: logging.error(f"Error processing log: {e}") -def parse_swap_logs(logs, watched_tokens): - total_amount_in = 0 - total_amount_out = 0 - token_addresses = [] + + # "Program log: Instruction: Swap2", + # "Program log: order_id: 13985890735038016", + # "Program log: AbrMJWfDVRZ2EWCQ1xSCpoVeVgZNpq1U2AoYG98oRXfn", source + # "Program log: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", target + +def parse_swap_logs(logs): + # Initialize variables to store the details + token_in = None + token_out = None + amount_in = 0 + amount_out_expected = 0 + amount_out_actual = 0 + + # Parse through each log entry for log in logs: - if "SwapEvent" in log: - parts = log.split("{ ")[1].strip(" }").split(", ") - event_details = {} - for part in parts: - key, value = part.split(": ") - event_details[key.strip()] = value.strip() - - # Aggregate amounts - total_amount_in += int(event_details.get("amount_in", 0)) - total_amount_out += int(event_details.get("amount_out", 0)) - - if "source_token_change:" in log: - # Extract final source and destination token changes + # Check for source and target tokens + if "Program log:" in log: + if "Swap2" in log: + # This log indicates the start of a swap, resetting details + token_in = None + token_out = None + elif "order_id" in log: + order_id = log.split("order_id: ")[-1] + else: + # Check for source and target tokens + if not token_in: + token_in = log.split("Program log: ")[-1].strip() + elif not token_out: + token_out = log.split("Program log: ")[-1].strip() + + # Example assuming token changes can be parsed as "source_token_change:" and "destination_token_change:" + elif "source_token_change:" in log: changes = log.split(", ") for change in changes: - key_value = change.split(": ", 1) - if len(key_value) != 2: - continue - key, value = key_value - if key == "source_token_change": - total_amount_in = int(value) - elif key == "destination_token_change": - total_amount_out = int(value) + if "source_token_change" in change: + amount_in = int(change.split(": ")[-1]) + elif "destination_token_change" in change: + amount_out_expected = int(change.split(": ")[-1]) + + # Assuming amount_out_actual is derived in a similar way to amount_out_expected + amount_out_actual = amount_out_expected # Modify if there is a separate way to determine actual amount - return { - "total_amount_in": total_amount_in, - "total_amount_out": total_amount_out, + # Return parsed details as a dictionary + return { + "token_in": token_in, + "token_out": token_out, + "amount_in": amount_in, + "amount_out_expected": amount_out_expected, + "amount_out_actual": amount_out_actual, + "amount_in_USD": amount_out_actual, # Assuming conversion logic elsewhere + "amount_out_USD": amount_out_actual, # Assuming conversion logic elsewhere } async def on_logs(log): From 413e8399d2a37083069a08972b85d67d987c2610 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Sat, 5 Oct 2024 21:54:06 +0300 Subject: [PATCH 09/21] tick --- crypto/sol/app.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/crypto/sol/app.py b/crypto/sol/app.py index 91fac0e..a577157 100644 --- a/crypto/sol/app.py +++ b/crypto/sol/app.py @@ -654,7 +654,7 @@ async def process_log(log_result): watched_tokens = await get_non_zero_token_balances(FOLLOWED_WALLET) - details = parse_swap_logs(logs) + details = await parse_swap_logs(logs) # transaction = await get_transaction_details_rpc(tx_signature_str, True) # tokens = [] @@ -729,32 +729,28 @@ async def process_log(log_result): # "Program log: AbrMJWfDVRZ2EWCQ1xSCpoVeVgZNpq1U2AoYG98oRXfn", source # "Program log: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", target -def parse_swap_logs(logs): - # Initialize variables to store the details +async def parse_swap_logs(logs): + global TOKEN_ADDRESSES + token_in = None token_out = None amount_in = 0 amount_out_expected = 0 amount_out_actual = 0 - - # Parse through each log entry + for log in logs: - # Check for source and target tokens if "Program log:" in log: if "Swap2" in log: - # This log indicates the start of a swap, resetting details token_in = None token_out = None elif "order_id" in log: order_id = log.split("order_id: ")[-1] else: - # Check for source and target tokens if not token_in: token_in = log.split("Program log: ")[-1].strip() elif not token_out: token_out = log.split("Program log: ")[-1].strip() - - # Example assuming token changes can be parsed as "source_token_change:" and "destination_token_change:" + elif "source_token_change:" in log: changes = log.split(", ") for change in changes: @@ -762,21 +758,25 @@ def parse_swap_logs(logs): amount_in = int(change.split(": ")[-1]) elif "destination_token_change" in change: amount_out_expected = int(change.split(": ")[-1]) - - # Assuming amount_out_actual is derived in a similar way to amount_out_expected - amount_out_actual = amount_out_expected # Modify if there is a separate way to determine actual amount - # Return parsed details as a dictionary - return { + amount_out_actual = amount_out_expected # Modify if actual is derived separately + + + token_prices = await get_token_prices([token_in, token_out]) + amount_in_usd = amount_in * token_prices.get(token_in, 0) + amount_out_usd = amount_out_actual * token_prices.get(token_out, 0) + + return { "token_in": token_in, "token_out": token_out, "amount_in": amount_in, "amount_out_expected": amount_out_expected, "amount_out_actual": amount_out_actual, - "amount_in_USD": amount_out_actual, # Assuming conversion logic elsewhere - "amount_out_USD": amount_out_actual, # Assuming conversion logic elsewhere + "amount_in_USD": amount_in_usd, + "amount_out_USD": amount_out_usd, } + async def on_logs(log): logging.debug(f"Received log: {log}") await save_log(log) From dc256e119a92d115eb3e2153e66cdaad19971a30 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Sat, 5 Oct 2024 22:39:35 +0300 Subject: [PATCH 10/21] initial follow move implemented! --- crypto/sol/app.py | 343 +++++++++++++--------------------------------- 1 file changed, 94 insertions(+), 249 deletions(-) diff --git a/crypto/sol/app.py b/crypto/sol/app.py index a577157..04e96f5 100644 --- a/crypto/sol/app.py +++ b/crypto/sol/app.py @@ -7,12 +7,16 @@ from solana.transaction import Signature from solana.rpc.websocket_api import connect from solana.rpc.types import TokenAccountOpts, TxOpts from solana.rpc.commitment import Confirmed +from base58 import b58decode +from solders.signature import Signature from solders.pubkey import Pubkey from solders.keypair import Keypair +from solders.transaction import VersionedTransaction from solders.transaction import Transaction from solders.message import Message from solders.instruction import Instruction from solders.hash import Hash +from jupiter_python_sdk.jupiter import Jupiter, Jupiter_DCA from dexscreener import DexscreenerClient from telegram import Bot from telegram.constants import ParseMode @@ -25,7 +29,7 @@ import aiohttp from typing import List, Dict import requests import threading - +import re load_dotenv() app = Flask(__name__) @@ -245,11 +249,6 @@ async def get_token_balance(wallet_address, token_address): commitment=Confirmed ) - - # response = await solana_client.get_token_accounts_by_owner( - # Pubkey.from_string(wallet_address), - # {"mint": Pubkey.from_string(token_address)} - # ) if response['result']['value']: balance = await solana_client.get_token_account_balance( response['result']['value'][0]['pubkey'] @@ -367,56 +366,9 @@ async def get_wallet_balances(wallet_address): return balances, token_addresses -async def get_converted_balances(wallet_address): - balances, token_addresses = await get_wallet_balances(wallet_address) - token_prices = await get_token_prices(token_addresses) - sol_price = await get_sol_price() - converted_balances = await convert_balances_to_currency(balances, token_prices, sol_price) - return converted_balances -async def send_initial_wallet_states(followed_wallet, your_wallet): - followed_balances = await get_converted_balances(followed_wallet) - your_balances = await get_converted_balances(your_wallet) - - message = f"Initial Wallet States (Non-zero balances in {DISPLAY_CURRENCY}):\n\n" - message += f"Followed Wallet ({followed_wallet}):\n" - for token, amount in followed_balances.items(): - if amount and amount > 0: - message += f"{token}: {amount:.2f}\n" - - message += f"\nYour Wallet ({your_wallet}):\n" - for token, amount in your_balances.items(): - if amount and amount > 0: - message += f"{token}: {amount:.2f}\n" - - message += "\nMonitored Tokens:\n" - # Add monitored tokens logic here if needed - - await bot.send_message(chat_id=CHAT_ID, text=message) - - - -async def get_non_zero_token_balances(wallet_address): - - followed_wallet_balances, followed_token_addresses = await get_wallet_balances(FOLLOWED_WALLET) - # return non zero balances for followed wallet - non_zero_balances = {token: address for token, address in {**followed_wallet_balances}.items() if address is not None and address > 0} - logging.info(f"Getting non-zero balances for wallet: {wallet_address}") - return non_zero_balances - - - # non_zero_balances = {} - # logging.info(f"Getting non-zero balances for wallet: {wallet_address}") - # for token, address in TOKEN_ADDRESSES.items(): - # balance = await get_token_balance_rpc(wallet_address, address) - # if balance > 0: - # non_zero_balances[token] = address - # logging.debug(f"Non-zero balance for {token}: {balance}") - # return non_zero_balances - - async def list_initial_wallet_states(): global TOKEN_ADDRESSES, FOLLOWED_WALLET_VALUE, YOUR_WALLET_VALUE @@ -463,101 +415,9 @@ async def list_initial_wallet_states(): await send_telegram_message(message) -async def follow_move(move): - followed_balances = await get_wallet_balances(FOLLOWED_WALLET) - your_balances = await get_wallet_balances(YOUR_WALLET) - if move['token'] not in followed_balances or move['token'] not in your_balances: - logging.error(f"Invalid token: {move['token']}") - return - followed_balance = followed_balances[move['token']] - your_balance = your_balances[move['token']] - proportion = your_balance / followed_balance if followed_balance > 0 else 0 - amount_to_swap = move['amount'] * proportion - - if your_balance >= amount_to_swap: - # Perform the swap using Jupiter API - try: - swap_result = perform_swap(move['token'], move['to_token'], amount_to_swap) - - if swap_result['success']: - message = ( - f"Move Followed:\n" - f"Swapped {amount_to_swap:.6f} {move['token']} " - f"for {swap_result['outputAmount']:.6f} {move['to_token']}" - ) - logging.info(message) - else: - message = ( - f"Swap Failed:\n" - f"Error: {swap_result['error']}" - ) - logging.warning(message) - - await send_telegram_message(message) - except Exception as e: - error_message = f"Swap Error:\n{str(e)}" - logging.error(error_message) - await send_telegram_message(error_message) - else: - message = ( - f"Move Failed:\n" - f"Insufficient balance to swap {amount_to_swap:.6f} {move['token']}" - ) - logging.warning(message) - await send_telegram_message(message) - -def perform_swap(input_token, output_token, amount): - # Jupiter API endpoint - url = "https://quote-api.jup.ag/v4/quote" - - # Parameters for the API request - params = { - "inputMint": input_token, - "outputMint": output_token, - "amount": int(amount * 10**9), # Convert to lamports - "slippageBps": 50, # 0.5% slippage - } - - try: - response = requests.get(url, params=params) - response.raise_for_status() - quote = response.json() - - # Get the best route - route = quote['data'][0] - - # Perform the swap - swap_url = "https://quote-api.jup.ag/v4/swap" - swap_data = { - "quoteResponse": route, - "userPublicKey": YOUR_WALLET, - "wrapUnwrapSOL": True - } - - swap_response = requests.post(swap_url, json=swap_data) - swap_response.raise_for_status() - swap_result = swap_response.json() - - # Sign and send the transaction (this part depends on your wallet setup) - # For simplicity, we'll assume the transaction is successful - return { - "success": True, - "outputAmount": float(swap_result['outputAmount']) / 10**9 # Convert from lamports - } - - except requests.exceptions.RequestException as e: - return { - "success": False, - "error": str(e) - } - -from base58 import b58decode -from solders.pubkey import Pubkey -from solders.transaction import Transaction -from solders.signature import Signature async def get_transaction_details_rpc(tx_signature, readfromDump=False): url = SOLANA_HTTP_URL @@ -609,34 +469,6 @@ async def save_log(log): logging.error(f"Error saving RPC log: {e}") -def determine_token(pubkey, watched_tokens): - # Check if the pubkey matches any watched token addresses - for token, address in watched_tokens.items(): - if pubkey == address: - return token - return "Unknown" - -def parse_amount_from_logs(logs): - amount_in = 0 - amount_out = 0 - - for log in logs: - if 'SwapEvent' in log: - # Extract amounts from the log line - parts = log.split('amount_in: ')[1].split(', amount_out: ') - amount_in = int(parts[0]) - amount_out = int(parts[1].split(' ')[0]) - - return amount_in, amount_out - -def extract_swap_details(instruction, logs, watched_tokens): - # Extract source and target tokens along with amounts - from_pubkey = instruction['accounts'][0] - to_pubkey = instruction['accounts'][1] - amount_in, amount_out = parse_amount_from_logs(logs) - return from_pubkey, to_pubkey, amount_in, amount_out - - async def process_log(log_result): if log_result['value']['err']: @@ -651,69 +483,18 @@ async def process_log(log_result): for log_entry in logs: if any(op in log_entry for op in swap_operations): try: - - - watched_tokens = await get_non_zero_token_balances(FOLLOWED_WALLET) details = await parse_swap_logs(logs) - # transaction = await get_transaction_details_rpc(tx_signature_str, True) - - # tokens = [] - # source_token = None - # target_token = None - - # # Check inner instructions for transfers and mints - # for instruction_set in transaction.get('meta', {}).get('innerInstructions', []): - # for instruction in instruction_set.get('instructions', []): - # if 'parsed' in instruction and 'info' in instruction['parsed']: - # info = instruction['parsed']['info'] - # amount = None - # mint = 'Unknown' - - # # Check for amount in transfer and transferChecked instructions - # if 'amount' in info: - # amount = info['amount'] - # elif 'tokenAmount' in info and 'amount' in info['tokenAmount']: - # amount = info['tokenAmount']['amount'] - - # # Get mint if available - # if 'mint' in info: - # mint = info['mint'] - - # if amount is not None: - # tokens.append({'amount': amount, 'mint': mint}) - - # # Identify source and target tokens - # if 'source' in info: - # source_token = info['source'] - # if 'destination' in info: - # target_token = info['destination'] - - # # Check post token balances for final token states - # for balance in transaction.get('postTokenBalances', []): - # amount = balance['uiTokenAmount']['amount'] - # mint = balance['mint'] - # tokens.append({'amount': amount, 'mint': mint}) - - # Get amount_in, amount_out, tokens, and USD value - swap_details = { - 'amount_in': details['total_amount_in'], - 'amount_out': details['total_amount_out'], - 'tokens': tokens, - 'source_token': source_token, - 'target_token': target_token - } - + message_text = ( f"Swap detected:\n" - f"Amount In: {swap_details['amount_in']}\n" - f"Amount Out: {swap_details['amount_out']}\n" - f"Source Token: {swap_details['source_token']}\n" - f"Target Token: {swap_details['target_token']}\n" - f"Tokens: {tokens}" + f"Order ID: {details['order_id']}\n" + f"Token In: {details['token_in']}\n" + f"Token Out: {details['token_out']}\n" + f"Amount In USD: {details['amount_in_USD']}\n" ) await send_telegram_message(message_text) - await follow_move(swap_details) + await follow_move(details) except Exception as e: logging.error(f"Error fetching transaction details: {e}") @@ -724,59 +505,123 @@ async def process_log(log_result): - # "Program log: Instruction: Swap2", + # "Program log: Instruction: Swap2", # "Program log: order_id: 13985890735038016", # "Program log: AbrMJWfDVRZ2EWCQ1xSCpoVeVgZNpq1U2AoYG98oRXfn", source # "Program log: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", target - + # "Program log: before_source_balance: 58730110139, before_destination_balance: 202377778, amount_in: 58730110139, expect_amount_out: 270109505, min_return: 267408410", + # "Program log: after_source_balance: 0, after_destination_balance: 472509072", + # "Program log: source_token_change: 58730110139, destination_token_change: 270131294", async def parse_swap_logs(logs): - global TOKEN_ADDRESSES - token_in = None token_out = None amount_in = 0 amount_out_expected = 0 amount_out_actual = 0 + order_id = None for log in logs: + # Check for token identifiers if "Program log:" in log: - if "Swap2" in log: + if "order_id:" in log: + order_id = log.split("order_id: ")[-1].strip() + elif "Swap2" in log: token_in = None token_out = None - elif "order_id" in log: - order_id = log.split("order_id: ")[-1] - else: - if not token_in: - token_in = log.split("Program log: ")[-1].strip() - elif not token_out: - token_out = log.split("Program log: ")[-1].strip() + elif not token_in: + token_in = log.split("Program log: ")[-1].strip() + elif not token_out: + token_out = log.split("Program log: ")[-1].strip() - elif "source_token_change:" in log: + # Use regex to find amount_in and amount_out patterns + if "amount_in" in log or "amount_out" in log: + amount_matches = re.findall(r"(amount_in|amount_out): (\d+)", log) + for amount_type, value in amount_matches: + if amount_type == "amount_in": + amount_in = int(value)/1e6 + elif amount_type == "amount_out": + amount_out_expected = int(value)/1e6 + + # Check for source_token_change and destination_token_change for additional details + elif "source_token_change:" in log or "destination_token_change:" in log: changes = log.split(", ") for change in changes: if "source_token_change" in change: - amount_in = int(change.split(": ")[-1]) + amount_in = int(change.split(": ")[-1])/1e6 elif "destination_token_change" in change: - amount_out_expected = int(change.split(": ")[-1]) - - amount_out_actual = amount_out_expected # Modify if actual is derived separately - + amount_out_actual = int(change.split(": ")[-1])/1e6 token_prices = await get_token_prices([token_in, token_out]) amount_in_usd = amount_in * token_prices.get(token_in, 0) amount_out_usd = amount_out_actual * token_prices.get(token_out, 0) return { + "order_id": order_id, "token_in": token_in, "token_out": token_out, "amount_in": amount_in, "amount_out_expected": amount_out_expected, "amount_out_actual": amount_out_actual, - "amount_in_USD": amount_in_usd, + "amount_in_USD": amount_in_usd , "amount_out_USD": amount_out_usd, } +async def follow_move(move): + followed_balances = await get_wallet_balances(FOLLOWED_WALLET) + your_balances = await get_wallet_balances(YOUR_WALLET) + + followed_balance = followed_balances[move.token_in] + your_balance = your_balances[move.token_in] + + proportion = your_balance / followed_balance if followed_balance > 0 else 0 + amount_to_swap = move.amount_in * proportion + + if your_balance >= amount_to_swap: + try: + # Initialize Jupiter client + private_key = Keypair.from_bytes(base58.b58decode(os.getenv("PK"))) + async_client = AsyncClient(SOLANA_WS_URL) + jupiter = Jupiter(async_client, private_key) + + # Perform the swap + transaction_data = await jupiter.swap( + input_mint=move.token_in, + output_mint=move.token_out, + amount=int(amount_to_swap), + slippage_bps=1, + ) + + raw_transaction = VersionedTransaction.from_bytes(base64.b64decode(transaction_data)) + signature = private_key.sign_message(message.to_bytes_versioned(raw_transaction.message)) + signed_txn = VersionedTransaction.populate(raw_transaction.message, [signature]) + opts = TxOpts(skip_preflight=False, preflight_commitment=Processed) + result = await async_client.send_raw_transaction(txn=bytes(signed_txn), opts=opts) + transaction_id = json.loads(result.to_json())['result'] + + message = ( + f"Move Followed:\n" + f"Swapped {amount_to_swap:.6f} {move['token']} " + f"for {transaction_data['outputAmount']:.6f} {move['to_token']}" + ) + logging.info(message) + await send_telegram_message(message) + except Exception as e: + error_message = f"Swap Error:\n{str(e)}" + logging.error(error_message) + await send_telegram_message(error_message) + else: + message = ( + f"Move Failed:\n" + f"Insufficient balance to swap {amount_to_swap:.6f} {move['token']}" + ) + logging.warning(message) + await send_telegram_message(message) + + + + + async def on_logs(log): logging.debug(f"Received log: {log}") await save_log(log) From 405445e6a8ea978c6cb3f3b354ded1c27a1b3642 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Sat, 5 Oct 2024 22:57:15 +0300 Subject: [PATCH 11/21] using percent in following amounts --- crypto/sol/.env.example | 15 +++++++ crypto/sol/app.py | 89 +++++++++++++++++++++-------------------- 2 files changed, 60 insertions(+), 44 deletions(-) create mode 100644 crypto/sol/.env.example diff --git a/crypto/sol/.env.example b/crypto/sol/.env.example new file mode 100644 index 0000000..e949837 --- /dev/null +++ b/crypto/sol/.env.example @@ -0,0 +1,15 @@ + +SOLANA_WS_URL="wss://api.mainnet-beta.solana.com" +SOLANA_WS_URL2="wss://mainnet.rpcpool.com" +SOLANA_HTTP_URL="https://api.mainnet-beta.solana.com" +DEVELOPER_CHAT_ID="777826553" +# Niki's +# FOLLOWED_WALLET="9U7D916zuQ8qcL9kQZqkcroWhHGho5vD8VNekvztrutN" +# My test Brave sync wallet +FOLLOWED_WALLET="7QXGLRjvyFAmxdRaP9Wk18KwWTMfspF4Na2sr3o3PzxV" + +TELEGRAM_BOT_TOKEN="6805059978:AAHNJKuOeazMSJHc3-BXRCsFfEVyFHeFnjw" +DISPLAY_CURRENCY=USD + +YOUR_WALLET="65nzyZXTLC81MthTo52a2gRJjqryTizWVqpK2fDKLye5" +PK={} \ No newline at end of file diff --git a/crypto/sol/app.py b/crypto/sol/app.py index 04e96f5..f8e8966 100644 --- a/crypto/sol/app.py +++ b/crypto/sol/app.py @@ -325,10 +325,17 @@ async def load_subscription_id(): return int(subscription_id) if subscription_id else None +async def get_token_name(mint_address): + try: + token_info = await solana_client.get_token_supply(Pubkey.from_string(mint_address)) + if token_info.value and 'symbol' in token_info.value: + return token_info.value['symbol'] + except Exception as e: + logging.error(f"Error fetching token name for {mint_address}: {str(e)}") + return None async def get_wallet_balances(wallet_address): balances = {} - token_addresses = [] logging.info(f"Getting balances for wallet: {wallet_address}") try: @@ -349,9 +356,9 @@ async def get_wallet_balances(wallet_address): mint = info['mint'] amount = float(info['tokenAmount']['uiAmount']) if amount > 0: - token_addresses.append(mint) - balances[mint] = amount - logging.debug(f"Balance for {mint}: {amount}") + token_name = await get_token_name(mint) or mint + balances[f"{token_name} ({mint})"] = amount + logging.debug(f"Balance for {token_name} ({mint}): {amount}") else: logging.warning(f"Unexpected data format for account: {account}") @@ -364,18 +371,15 @@ async def get_wallet_balances(wallet_address): except Exception as e: logging.error(f"Error getting wallet balances: {str(e)}") - return balances, token_addresses - - - + return balances async def list_initial_wallet_states(): global TOKEN_ADDRESSES, FOLLOWED_WALLET_VALUE, YOUR_WALLET_VALUE - followed_wallet_balances, followed_token_addresses = await get_wallet_balances(FOLLOWED_WALLET) - your_wallet_balances, your_token_addresses = await get_wallet_balances(YOUR_WALLET) + followed_wallet_balances = await get_wallet_balances(FOLLOWED_WALLET) + your_wallet_balances = await get_wallet_balances(YOUR_WALLET) - all_token_addresses = list(set(followed_token_addresses + your_token_addresses)) + all_token_addresses = list(set(followed_wallet_balances.keys()) | set(your_wallet_balances.keys())) token_prices = await get_token_prices(all_token_addresses) sol_price = await get_sol_price() @@ -415,10 +419,6 @@ async def list_initial_wallet_states(): await send_telegram_message(message) - - - - async def get_transaction_details_rpc(tx_signature, readfromDump=False): url = SOLANA_HTTP_URL # url = 'https://solana.drpc.org' @@ -491,6 +491,7 @@ async def process_log(log_result): f"Token In: {details['token_in']}\n" f"Token Out: {details['token_out']}\n" f"Amount In USD: {details['amount_in_USD']}\n" + f"Percentage Swapped: {details['percentage_swapped']:.2f}%" ) await send_telegram_message(message_text) @@ -519,9 +520,9 @@ async def parse_swap_logs(logs): amount_out_expected = 0 amount_out_actual = 0 order_id = None + before_source_balance = 0 for log in logs: - # Check for token identifiers if "Program log:" in log: if "order_id:" in log: order_id = log.split("order_id: ")[-1].strip() @@ -533,62 +534,61 @@ async def parse_swap_logs(logs): elif not token_out: token_out = log.split("Program log: ")[-1].strip() - # Use regex to find amount_in and amount_out patterns + if "before_source_balance:" in log: + before_source_balance = int(re.search(r"before_source_balance: (\d+)", log).group(1)) + if "amount_in" in log or "amount_out" in log: amount_matches = re.findall(r"(amount_in|amount_out): (\d+)", log) for amount_type, value in amount_matches: if amount_type == "amount_in": - amount_in = int(value)/1e6 + amount_in = int(value) elif amount_type == "amount_out": - amount_out_expected = int(value)/1e6 + amount_out_expected = int(value) - # Check for source_token_change and destination_token_change for additional details elif "source_token_change:" in log or "destination_token_change:" in log: changes = log.split(", ") for change in changes: if "source_token_change" in change: - amount_in = int(change.split(": ")[-1])/1e6 + amount_in = int(change.split(": ")[-1]) elif "destination_token_change" in change: - amount_out_actual = int(change.split(": ")[-1])/1e6 + amount_out_actual = int(change.split(": ")[-1]) token_prices = await get_token_prices([token_in, token_out]) - amount_in_usd = amount_in * token_prices.get(token_in, 0) - amount_out_usd = amount_out_actual * token_prices.get(token_out, 0) + amount_in_usd = amount_in / 1e6 * token_prices.get(token_in, 0) + amount_out_usd = amount_out_actual / 1e6 * token_prices.get(token_out, 0) + + # Calculate the percentage of the source balance that was swapped + percentage_swapped = (amount_in / before_source_balance) * 100 if before_source_balance > 0 else 0 return { "order_id": order_id, "token_in": token_in, "token_out": token_out, - "amount_in": amount_in, - "amount_out_expected": amount_out_expected, - "amount_out_actual": amount_out_actual, - "amount_in_USD": amount_in_usd , + "amount_in": amount_in / 1e6, + "amount_out_expected": amount_out_expected / 1e6, + "amount_out_actual": amount_out_actual / 1e6, + "amount_in_USD": amount_in_usd, "amount_out_USD": amount_out_usd, + "percentage_swapped": percentage_swapped } - async def follow_move(move): - followed_balances = await get_wallet_balances(FOLLOWED_WALLET) your_balances = await get_wallet_balances(YOUR_WALLET) + your_balance = your_balances.get(move['token_in'], 0) - followed_balance = followed_balances[move.token_in] - your_balance = your_balances[move.token_in] - - proportion = your_balance / followed_balance if followed_balance > 0 else 0 - amount_to_swap = move.amount_in * proportion + # Calculate the amount to swap based on the same percentage as the followed move + amount_to_swap = your_balance * (move['percentage_swapped'] / 100) if your_balance >= amount_to_swap: try: - # Initialize Jupiter client private_key = Keypair.from_bytes(base58.b58decode(os.getenv("PK"))) async_client = AsyncClient(SOLANA_WS_URL) jupiter = Jupiter(async_client, private_key) - # Perform the swap transaction_data = await jupiter.swap( - input_mint=move.token_in, - output_mint=move.token_out, - amount=int(amount_to_swap), + input_mint=move['token_in'], + output_mint=move['token_out'], + amount=int(amount_to_swap * 1e6), # Convert to lamports slippage_bps=1, ) @@ -601,8 +601,9 @@ async def follow_move(move): message = ( f"Move Followed:\n" - f"Swapped {amount_to_swap:.6f} {move['token']} " - f"for {transaction_data['outputAmount']:.6f} {move['to_token']}" + f"Swapped {amount_to_swap:.6f} {move['token_in']} " + f"(same {move['percentage_swapped']:.2f}% as followed wallet)\n" + f"for {transaction_data['outputAmount'] / 1e6:.6f} {move['token_out']}" ) logging.info(message) await send_telegram_message(message) @@ -613,12 +614,12 @@ async def follow_move(move): else: message = ( f"Move Failed:\n" - f"Insufficient balance to swap {amount_to_swap:.6f} {move['token']}" + f"Insufficient balance to swap {amount_to_swap:.6f} {move['token_in']}" ) logging.warning(message) await send_telegram_message(message) - +# Helper functions (implement these according to your needs) From 2a15df144cb2160839eb23d720af8679d7c74653 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Sat, 5 Oct 2024 23:00:20 +0300 Subject: [PATCH 12/21] also get token names --- crypto/sol/app.py | 157 +++++++++++++++++++++++++--------------------- 1 file changed, 84 insertions(+), 73 deletions(-) diff --git a/crypto/sol/app.py b/crypto/sol/app.py index f8e8966..62a4e82 100644 --- a/crypto/sol/app.py +++ b/crypto/sol/app.py @@ -34,6 +34,19 @@ import re load_dotenv() app = Flask(__name__) +ENV_FILE = '.env' + +async def save_subscription_id(subscription_id): + # storing subscription id in .env file disabled + #set_key(ENV_FILE, "SUBSCRIPTION_ID", str(subscription_id)) + logger.info(f"Saved subscription ID: {subscription_id}") + +async def load_subscription_id(): + subscription_id = os.getenv("SUBSCRIPTION_ID") + return int(subscription_id) if subscription_id else None + + + # Function to find the latest log file def get_latest_log_file(): log_dir = './logs' @@ -103,35 +116,6 @@ async def send_telegram_message(message): logging.error(f"Error sending Telegram message: {str(e)}") -# async def get_token_prices(token_addresses: List[str]) -> Dict[str, float]: -# url = "https://api.coingecko.com/api/v3/simple/token_price/solana" -# params = { -# "contract_addresses": ",".join(token_addresses), -# "vs_currencies": DISPLAY_CURRENCY.lower() -# } -# prices = {} - -# async with aiohttp.ClientSession() as session: -# async with session.get(url, params=params) as response: -# if response.status == 200: -# data = await response.json() -# for address, price_info in data.items(): -# if DISPLAY_CURRENCY.lower() in price_info: -# prices[address] = price_info[DISPLAY_CURRENCY.lower()] -# else: -# logging.error(f"Failed to get token prices. Status: {response.status}") - -# # For tokens not found in CoinGecko, try to get price from a DEX or set a default value -# missing_tokens = set(token_addresses) - set(prices.keys()) -# for token in missing_tokens: -# # You might want to implement a fallback method here, such as: -# # prices[token] = await get_price_from_dex(token) -# # For now, we'll set a default value -# prices[token] = 0.0 -# logging.warning(f"Price not found for token {token}. Setting to 0.") - -# return prices - async def get_token_prices(token_addresses: List[str]) -> Dict[str, float]: coingecko_prices = await get_prices_from_coingecko(token_addresses) @@ -237,33 +221,6 @@ async def convert_balances_to_currency(balances, token_prices, sol_price): return converted_balances - -async def get_token_balance(wallet_address, token_address): - try: - response = await solana_client.get_token_accounts_by_owner_json_parsed( - Pubkey.from_string(wallet_address), - opts=TokenAccountOpts( - mint=Pubkey.from_string(token_address), - # program_id=Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") - ), - commitment=Confirmed - ) - - if response['result']['value']: - balance = await solana_client.get_token_account_balance( - response['result']['value'][0]['pubkey'] - ) - amount = float(balance['result']['value']['uiAmount']) - logging.debug(f"Balance for {token_address} in {wallet_address}: {amount}") - return amount - else: - logging.debug(f"No account found for {token_address} in {wallet_address}") - return 0 - except Exception as e: - logging.error(f"Error getting balance for {token_address} in {wallet_address}: {str(e)} \r\n {e}") - return 0 - - async def get_token_balance_rpc(wallet_address, token_address): url = SOLANA_HTTP_URL headers = {"Content-Type": "application/json"} @@ -313,16 +270,8 @@ async def get_token_balance_rpc(wallet_address, token_address): except requests.exceptions.RequestException as e: logging.error(f"Error getting balance for {token_address} in {wallet_address}: {str(e)} \r\n {e}") return 0 -ENV_FILE = '.env' + -async def save_subscription_id(subscription_id): - # storing subscription id in .env file disabled - #set_key(ENV_FILE, "SUBSCRIPTION_ID", str(subscription_id)) - logger.info(f"Saved subscription ID: {subscription_id}") - -async def load_subscription_id(): - subscription_id = os.getenv("SUBSCRIPTION_ID") - return int(subscription_id) if subscription_id else None async def get_token_name(mint_address): @@ -356,15 +305,23 @@ async def get_wallet_balances(wallet_address): mint = info['mint'] amount = float(info['tokenAmount']['uiAmount']) if amount > 0: - token_name = await get_token_name(mint) or mint - balances[f"{token_name} ({mint})"] = amount + token_name = await get_token_name(mint) or 'Unknown' + balances[mint] = { + 'name': token_name, + 'address': mint, + 'amount': amount + } logging.debug(f"Balance for {token_name} ({mint}): {amount}") else: logging.warning(f"Unexpected data format for account: {account}") sol_balance = await solana_client.get_balance(Pubkey.from_string(wallet_address)) if sol_balance.value is not None: - balances['SOL'] = sol_balance.value / 1e9 + balances['SOL'] = { + 'name': 'SOL', + 'address': 'SOL', + 'amount': sol_balance.value / 1e9 + } else: logging.warning(f"SOL balance response missing for wallet: {wallet_address}") @@ -386,6 +343,49 @@ async def list_initial_wallet_states(): followed_converted_balances = await convert_balances_to_currency(followed_wallet_balances, token_prices, sol_price) your_converted_balances = await convert_balances_to_currency(your_wallet_balances, token_prices, sol_price) + TOKEN_ADDRESSES = {token: balance['amount'] for token, balance in {**followed_converted_balances, **your_converted_balances}.items() if balance['amount'] is not None and balance['amount'] > 0} + logging.info(f"Monitoring balances for tokens: {[balance['name'] for balance in TOKEN_ADDRESSES.values()]}") + + followed_wallet_state = [] + FOLLOWED_WALLET_VALUE = 0 + for token, balance in followed_converted_balances.items(): + if balance['amount'] is not None and balance['amount'] > 0: + followed_wallet_state.append(f"{balance['name']} ({balance['address']}): {balance['amount']:.2f} {DISPLAY_CURRENCY}") + FOLLOWED_WALLET_VALUE += balance['amount'] + + your_wallet_state = [] + YOUR_WALLET_VALUE = 0 + for token, balance in your_converted_balances.items(): + if balance['amount'] is not None and balance['amount'] > 0: + your_wallet_state.append(f"{balance['name']} ({balance['address']}): {balance['amount']:.2f} {DISPLAY_CURRENCY}") + YOUR_WALLET_VALUE += balance['amount'] + + message = ( + f"Initial Wallet States (All balances in {DISPLAY_CURRENCY}):\n\n" + f"Followed Wallet ({FOLLOWED_WALLET}):\n" + f"{chr(10).join(followed_wallet_state)}\n" + f"Total Value: {FOLLOWED_WALLET_VALUE:.2f} {DISPLAY_CURRENCY}\n\n" + f"Your Wallet ({YOUR_WALLET}):\n" + f"{chr(10).join(your_wallet_state)}\n" + f"Total Value: {YOUR_WALLET_VALUE:.2f} {DISPLAY_CURRENCY}\n\n" + f"Monitored Tokens:\n" + f"{', '.join([balance['name'] for balance in TOKEN_ADDRESSES.values()])}" + ) + + logging.info(message) + await send_telegram_message(message) + global TOKEN_ADDRESSES, FOLLOWED_WALLET_VALUE, YOUR_WALLET_VALUE + + followed_wallet_balances = await get_wallet_balances(FOLLOWED_WALLET) + your_wallet_balances = await get_wallet_balances(YOUR_WALLET) + + all_token_addresses = list(set(followed_wallet_balances.keys()) | set(your_wallet_balances.keys())) + token_prices = await get_token_prices(all_token_addresses) + sol_price = await get_sol_price() + + followed_converted_balances = await convert_balances_to_currency(followed_wallet_balances, token_prices, sol_price) + your_converted_balances = await convert_balances_to_currency(your_wallet_balances, token_prices, sol_price) + TOKEN_ADDRESSES = {token: amount for token, amount in {**followed_converted_balances, **your_converted_balances}.items() if amount is not None and amount > 0} logging.info(f"Monitoring balances for tokens: {TOKEN_ADDRESSES.keys()}") @@ -574,7 +574,16 @@ async def parse_swap_logs(logs): async def follow_move(move): your_balances = await get_wallet_balances(YOUR_WALLET) - your_balance = your_balances.get(move['token_in'], 0) + your_balance_info = your_balances.get(move['token_in']) + + if not your_balance_info: + message = f"Move Failed:\nNo balance found for token {move['token_in']}" + logging.warning(message) + await send_telegram_message(message) + return + + your_balance = your_balance_info['amount'] + token_name = your_balance_info['name'] # Calculate the amount to swap based on the same percentage as the followed move amount_to_swap = your_balance * (move['percentage_swapped'] / 100) @@ -599,11 +608,14 @@ async def follow_move(move): result = await async_client.send_raw_transaction(txn=bytes(signed_txn), opts=opts) transaction_id = json.loads(result.to_json())['result'] + output_token_info = your_balances.get(move['token_out'], {'name': 'Unknown'}) + output_token_name = output_token_info['name'] + message = ( f"Move Followed:\n" - f"Swapped {amount_to_swap:.6f} {move['token_in']} " + f"Swapped {amount_to_swap:.6f} {token_name} ({move['token_in']}) " f"(same {move['percentage_swapped']:.2f}% as followed wallet)\n" - f"for {transaction_data['outputAmount'] / 1e6:.6f} {move['token_out']}" + f"for {transaction_data['outputAmount'] / 1e6:.6f} {output_token_name} ({move['token_out']})" ) logging.info(message) await send_telegram_message(message) @@ -614,11 +626,10 @@ async def follow_move(move): else: message = ( f"Move Failed:\n" - f"Insufficient balance to swap {amount_to_swap:.6f} {move['token_in']}" + f"Insufficient balance to swap {amount_to_swap:.6f} {token_name} ({move['token_in']})" ) logging.warning(message) await send_telegram_message(message) - # Helper functions (implement these according to your needs) From c16129bcf04cd56ab193818875ed86081676c995 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Sat, 5 Oct 2024 23:18:25 +0300 Subject: [PATCH 13/21] follow move works --- crypto/sol/app.py | 62 ++++++++--------------------------------------- 1 file changed, 10 insertions(+), 52 deletions(-) diff --git a/crypto/sol/app.py b/crypto/sol/app.py index 62a4e82..59d008c 100644 --- a/crypto/sol/app.py +++ b/crypto/sol/app.py @@ -210,14 +210,16 @@ async def get_sol_price() -> float: async def convert_balances_to_currency(balances, token_prices, sol_price): converted_balances = {} - for token, amount in balances.items(): - if token == 'SOL': - converted_balances[token] = amount * sol_price - elif token in token_prices: - converted_balances[token] = amount * token_prices[token] + for address, info in balances.items(): + converted_balance = info.copy() # Create a copy of the original info + if info['name'] == 'SOL': + converted_balance['value'] = info['amount'] * sol_price + elif address in token_prices: + converted_balance['value'] = info['amount'] * token_prices[address] else: - converted_balances[token] = None # Price not available - logging.warning(f"Price not available for token {token}") + converted_balance['value'] = None # Price not available + logging.warning(f"Price not available for token {info['name']} ({address})") + converted_balances[address] = converted_balance return converted_balances @@ -374,50 +376,6 @@ async def list_initial_wallet_states(): logging.info(message) await send_telegram_message(message) - global TOKEN_ADDRESSES, FOLLOWED_WALLET_VALUE, YOUR_WALLET_VALUE - - followed_wallet_balances = await get_wallet_balances(FOLLOWED_WALLET) - your_wallet_balances = await get_wallet_balances(YOUR_WALLET) - - all_token_addresses = list(set(followed_wallet_balances.keys()) | set(your_wallet_balances.keys())) - token_prices = await get_token_prices(all_token_addresses) - sol_price = await get_sol_price() - - followed_converted_balances = await convert_balances_to_currency(followed_wallet_balances, token_prices, sol_price) - your_converted_balances = await convert_balances_to_currency(your_wallet_balances, token_prices, sol_price) - - TOKEN_ADDRESSES = {token: amount for token, amount in {**followed_converted_balances, **your_converted_balances}.items() if amount is not None and amount > 0} - logging.info(f"Monitoring balances for tokens: {TOKEN_ADDRESSES.keys()}") - - followed_wallet_state = [] - FOLLOWED_WALLET_VALUE = 0 - for token, amount in followed_converted_balances.items(): - if amount is not None and amount > 0: - followed_wallet_state.append(f"{token}: {amount:.2f} {DISPLAY_CURRENCY}") - FOLLOWED_WALLET_VALUE += amount - - your_wallet_state = [] - YOUR_WALLET_VALUE = 0 - for token, amount in your_converted_balances.items(): - if amount is not None and amount > 0: - your_wallet_state.append(f"{token}: {amount:.2f} {DISPLAY_CURRENCY}") - YOUR_WALLET_VALUE += amount - - message = ( - f"Initial Wallet States (All balances in {DISPLAY_CURRENCY}):\n\n" - f"Followed Wallet ({FOLLOWED_WALLET}):\n" - f"{chr(10).join(followed_wallet_state)}\n" - f"Total Value: {FOLLOWED_WALLET_VALUE:.2f} {DISPLAY_CURRENCY}\n\n" - f"Your Wallet ({YOUR_WALLET}):\n" - f"{chr(10).join(your_wallet_state)}\n" - f"Total Value: {YOUR_WALLET_VALUE:.2f} {DISPLAY_CURRENCY}\n\n" - f"Monitored Tokens:\n" - f"{', '.join(TOKEN_ADDRESSES.keys())}" - ) - - logging.info(message) - await send_telegram_message(message) - async def get_transaction_details_rpc(tx_signature, readfromDump=False): url = SOLANA_HTTP_URL @@ -574,7 +532,7 @@ async def parse_swap_logs(logs): async def follow_move(move): your_balances = await get_wallet_balances(YOUR_WALLET) - your_balance_info = your_balances.get(move['token_in']) + your_balance_info = next((balance for balance in your_balances.values() if balance['address'] == move['token_in']), None) if not your_balance_info: message = f"Move Failed:\nNo balance found for token {move['token_in']}" From 774bd333efdd4418f27cb52df985f9a150c0b744 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Sun, 6 Oct 2024 01:05:50 +0300 Subject: [PATCH 14/21] tick --- crypto/sol/app.py | 153 +++++++++++++++++++++++++--------------------- 1 file changed, 84 insertions(+), 69 deletions(-) diff --git a/crypto/sol/app.py b/crypto/sol/app.py index 59d008c..36f7643 100644 --- a/crypto/sol/app.py +++ b/crypto/sol/app.py @@ -30,6 +30,7 @@ from typing import List, Dict import requests import threading import re +from typing import List, Dict, Any, Tuple load_dotenv() app = Flask(__name__) @@ -208,20 +209,6 @@ async def get_sol_price() -> float: logging.error(f"Failed to get SOL price. Status: {response.status}") return None -async def convert_balances_to_currency(balances, token_prices, sol_price): - converted_balances = {} - for address, info in balances.items(): - converted_balance = info.copy() # Create a copy of the original info - if info['name'] == 'SOL': - converted_balance['value'] = info['amount'] * sol_price - elif address in token_prices: - converted_balance['value'] = info['amount'] * token_prices[address] - else: - converted_balance['value'] = None # Price not available - logging.warning(f"Price not available for token {info['name']} ({address})") - converted_balances[address] = converted_balance - return converted_balances - async def get_token_balance_rpc(wallet_address, token_address): url = SOLANA_HTTP_URL @@ -332,6 +319,20 @@ async def get_wallet_balances(wallet_address): return balances +async def convert_balances_to_currency(balances, token_prices, sol_price): + converted_balances = {} + for address, info in balances.items(): + converted_balance = info.copy() # Create a copy of the original info + if info['name'] == 'SOL': + converted_balance['value'] = info['amount'] * sol_price + elif address in token_prices: + converted_balance['value'] = info['amount'] * token_prices[address] + else: + converted_balance['value'] = None # Price not available + logging.warning(f"Price not available for token {info['name']} ({address})") + converted_balances[address] = converted_balance + return converted_balances + async def list_initial_wallet_states(): global TOKEN_ADDRESSES, FOLLOWED_WALLET_VALUE, YOUR_WALLET_VALUE @@ -345,22 +346,22 @@ async def list_initial_wallet_states(): followed_converted_balances = await convert_balances_to_currency(followed_wallet_balances, token_prices, sol_price) your_converted_balances = await convert_balances_to_currency(your_wallet_balances, token_prices, sol_price) - TOKEN_ADDRESSES = {token: balance['amount'] for token, balance in {**followed_converted_balances, **your_converted_balances}.items() if balance['amount'] is not None and balance['amount'] > 0} - logging.info(f"Monitoring balances for tokens: {[balance['name'] for balance in TOKEN_ADDRESSES.values()]}") + TOKEN_ADDRESSES = {address: info for address, info in {**followed_converted_balances, **your_converted_balances}.items() if info['value'] is not None and info['value'] > 0} + logging.info(f"Monitoring balances for tokens: {[info['name'] for info in TOKEN_ADDRESSES.values()]}") followed_wallet_state = [] FOLLOWED_WALLET_VALUE = 0 - for token, balance in followed_converted_balances.items(): - if balance['amount'] is not None and balance['amount'] > 0: - followed_wallet_state.append(f"{balance['name']} ({balance['address']}): {balance['amount']:.2f} {DISPLAY_CURRENCY}") - FOLLOWED_WALLET_VALUE += balance['amount'] + for address, info in followed_converted_balances.items(): + if info['value'] is not None and info['value'] > 0: + followed_wallet_state.append(f"{info['name']} ({address}): {info['value']:.2f} {DISPLAY_CURRENCY}") + FOLLOWED_WALLET_VALUE += info['value'] your_wallet_state = [] YOUR_WALLET_VALUE = 0 - for token, balance in your_converted_balances.items(): - if balance['amount'] is not None and balance['amount'] > 0: - your_wallet_state.append(f"{balance['name']} ({balance['address']}): {balance['amount']:.2f} {DISPLAY_CURRENCY}") - YOUR_WALLET_VALUE += balance['amount'] + for address, info in your_converted_balances.items(): + if info['value'] is not None and info['value'] > 0: + your_wallet_state.append(f"{info['name']} ({address}): {info['value']:.2f} {DISPLAY_CURRENCY}") + YOUR_WALLET_VALUE += info['value'] message = ( f"Initial Wallet States (All balances in {DISPLAY_CURRENCY}):\n\n" @@ -371,12 +372,13 @@ async def list_initial_wallet_states(): f"{chr(10).join(your_wallet_state)}\n" f"Total Value: {YOUR_WALLET_VALUE:.2f} {DISPLAY_CURRENCY}\n\n" f"Monitored Tokens:\n" - f"{', '.join([balance['name'] for balance in TOKEN_ADDRESSES.values()])}" + f"{', '.join([info['name'] for info in TOKEN_ADDRESSES.values()])}" ) logging.info(message) await send_telegram_message(message) + async def get_transaction_details_rpc(tx_signature, readfromDump=False): url = SOLANA_HTTP_URL # url = 'https://solana.drpc.org' @@ -407,7 +409,7 @@ async def get_transaction_details_rpc(tx_signature, readfromDump=False): if 'result' in transaction_details: - print(transaction_details['result']) + #print(transaction_details['result']) return transaction_details['result'] else: print("Unexpected response:", transaction_details) @@ -432,7 +434,7 @@ async def process_log(log_result): if log_result['value']['err']: return - tx_signature_str = log_result['value']['signature'] + logs = log_result['value']['logs'] try: # Detect swap operations in logs @@ -441,7 +443,8 @@ async def process_log(log_result): for log_entry in logs: if any(op in log_entry for op in swap_operations): try: - details = await parse_swap_logs(logs) + tx_signature_str = log_result['value']['signature'] + details = await parse_swap_logs(tx_signature_str, logs) message_text = ( f"Swap detected:\n" @@ -471,49 +474,25 @@ async def process_log(log_result): # "Program log: before_source_balance: 58730110139, before_destination_balance: 202377778, amount_in: 58730110139, expect_amount_out: 270109505, min_return: 267408410", # "Program log: after_source_balance: 0, after_destination_balance: 472509072", # "Program log: source_token_change: 58730110139, destination_token_change: 270131294", -async def parse_swap_logs(logs): +async def parse_swap_logs(tx_signature_str: str, logs: List[str]) -> Dict[str, Any]: token_in = None token_out = None amount_in = 0 - amount_out_expected = 0 - amount_out_actual = 0 + amount_out = 0 order_id = None - before_source_balance = 0 - for log in logs: - if "Program log:" in log: - if "order_id:" in log: - order_id = log.split("order_id: ")[-1].strip() - elif "Swap2" in log: - token_in = None - token_out = None - elif not token_in: - token_in = log.split("Program log: ")[-1].strip() - elif not token_out: - token_out = log.split("Program log: ")[-1].strip() - - if "before_source_balance:" in log: - before_source_balance = int(re.search(r"before_source_balance: (\d+)", log).group(1)) - - if "amount_in" in log or "amount_out" in log: - amount_matches = re.findall(r"(amount_in|amount_out): (\d+)", log) - for amount_type, value in amount_matches: - if amount_type == "amount_in": - amount_in = int(value) - elif amount_type == "amount_out": - amount_out_expected = int(value) - - elif "source_token_change:" in log or "destination_token_change:" in log: - changes = log.split(", ") - for change in changes: - if "source_token_change" in change: - amount_in = int(change.split(": ")[-1]) - elif "destination_token_change" in change: - amount_out_actual = int(change.split(": ")[-1]) + transaction_details = await get_transaction_details_rpc(tx_signature_str) + token_in, token_out, amount_in, amount_out = _extract_token_info(transaction_details) + # Fetch token prices token_prices = await get_token_prices([token_in, token_out]) - amount_in_usd = amount_in / 1e6 * token_prices.get(token_in, 0) - amount_out_usd = amount_out_actual / 1e6 * token_prices.get(token_out, 0) + + # Calculate USD values + amount_in_usd = amount_in * token_prices.get(token_in, 0) + amount_out_usd = amount_out * token_prices.get(token_out, 0) + + # Get the pre-balance of the input token + before_source_balance = _get_pre_balance(transaction_details, token_in) # Calculate the percentage of the source balance that was swapped percentage_swapped = (amount_in / before_source_balance) * 100 if before_source_balance > 0 else 0 @@ -522,14 +501,48 @@ async def parse_swap_logs(logs): "order_id": order_id, "token_in": token_in, "token_out": token_out, - "amount_in": amount_in / 1e6, - "amount_out_expected": amount_out_expected / 1e6, - "amount_out_actual": amount_out_actual / 1e6, + "amount_in": amount_in, + "amount_out": amount_out, "amount_in_USD": amount_in_usd, "amount_out_USD": amount_out_usd, "percentage_swapped": percentage_swapped } +def _extract_token_info(transaction_details: Dict[str, Any]) -> Tuple[str, str, float, float]: + inner_instructions = transaction_details.get('meta', {}).get('innerInstructions', []) + token_in = None + token_out = None + amount_in = 0 + amount_out = 0 + + for instruction_set in inner_instructions: + for instruction in instruction_set.get('instructions', []): + if 'parsed' in instruction and 'info' in instruction['parsed']: + info = instruction['parsed']['info'] + if info.get('type') == 'transfer': + if token_in is None: + token_in = info.get('source') + amount_in = float(info.get('amount', 0)) + else: + token_out = info.get('destination') + amount_out = float(info.get('amount', 0)) + + return token_in, token_out, amount_in, amount_out + +def _get_pre_balance(transaction_details: Dict[str, Any], token: str) -> float: + pre_balances = transaction_details.get('meta', {}).get('preTokenBalances', []) + for balance in pre_balances: + if balance['mint'] == token: + return float(balance['uiTokenAmount']['amount']) + return 0.0 +def _get_pre_balance(transaction_details: Dict[str, Any], token: str) -> float: + pre_balances = transaction_details.get('meta', {}).get('preTokenBalances', []) + for balance in pre_balances: + if balance['mint'] == token: + return float(balance['uiTokenAmount']['amount']) + return 0.0 + + async def follow_move(move): your_balances = await get_wallet_balances(YOUR_WALLET) your_balance_info = next((balance for balance in your_balances.values() if balance['address'] == move['token_in']), None) @@ -609,6 +622,9 @@ async def subscribe_to_wallet(): reconnect_delay = 5 # Start with a 5-second delay max_reconnect_delay = 60 # Maximum delay of 60 seconds + + await list_initial_wallet_states() + while True: try: async with websockets.connect(uri) as websocket: @@ -644,7 +660,6 @@ async def subscribe_to_wallet(): await save_subscription_id(subscription_id) logger.info(f"Subscription successful. Subscription id: {subscription_id}") await send_telegram_message("Connected to Solana network. Watching for transactions now.") - await list_initial_wallet_states() elif 'params' in response_data: await on_logs(response_data['params']['result']) @@ -679,7 +694,7 @@ async def main(): # Initialize logging logging.basicConfig(level=logging.DEBUG) await send_telegram_message("Solana Agent Started. Connecting to mainnet...") - await subscribe_to_wallet() + #await subscribe_to_wallet() def run_flask(): # Run Flask app without the reloader, so we can run the async main function From 14f3674da06a8a846a6a8e99dfd84f1cbea0aeb7 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Mon, 7 Oct 2024 00:16:09 +0300 Subject: [PATCH 15/21] proceed rtying to parse solana tr logs --- crypto/sol/app.py | 156 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 132 insertions(+), 24 deletions(-) diff --git a/crypto/sol/app.py b/crypto/sol/app.py index 36f7643..8f39c2d 100644 --- a/crypto/sol/app.py +++ b/crypto/sol/app.py @@ -7,7 +7,10 @@ from solana.transaction import Signature from solana.rpc.websocket_api import connect from solana.rpc.types import TokenAccountOpts, TxOpts from solana.rpc.commitment import Confirmed -from base58 import b58decode +from solana.transaction import Transaction +from base64 import b64decode +import base58 +from solders.rpc.requests import GetTransaction from solders.signature import Signature from solders.pubkey import Pubkey from solders.keypair import Keypair @@ -16,6 +19,7 @@ from solders.transaction import Transaction from solders.message import Message from solders.instruction import Instruction from solders.hash import Hash +from solders.instruction import CompiledInstruction from jupiter_python_sdk.jupiter import Jupiter, Jupiter_DCA from dexscreener import DexscreenerClient from telegram import Bot @@ -378,6 +382,76 @@ async def list_initial_wallet_states(): logging.info(message) await send_telegram_message(message) +async def get_swap_transaction_details(tx_signature_str): + t = await solana_client.get_transaction(Signature.from_string(tx_signature_str), max_supported_transaction_version=0) + try: + transaction = t.value.transaction + message = transaction.transaction.message + instructions = message.instructions + + accounts = t.value.transaction.transaction.message.instructions[0].accounts + instructions = t.value.transaction.transaction.message.instructions + + # Assume the swap is the first instruction + swap_instruction = instructions[0] + + # Extract accounts involved in the swap instruction + accounts = swap_instruction.accounts + + # Initialize result dictionary + parsed_result = { + "order_id": None, + "token_in": None, + "token_out": None, + "amount_in": 0, + "amount_out": 0, + "amount_in_USD": 0, + "amount_out_USD": 0, + "percentage_swapped": 0 + } + + # Extract log messages for order_id and swap details. we have that in the log hook + # log_messages = t.value.meta.log_messages + # for log in log_messages: + # if "order_id" in log: + # parsed_result["order_id"] = log.split(":")[1].strip() + # break + + # Parse the swap instruction to extract token addresses, amounts, and types + for instruction in instructions: + if isinstance(instruction, CompiledInstruction): + if instruction.program_id == Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"): + parsed_info = instruction.parsed.info + mint = parsed_info["mint"] + amount = float(parsed_info["tokenAmount"]["amount"]) / (10 ** parsed_info["tokenAmount"]["decimals"]) + + # Determine token in and token out based on balances + if parsed_result["token_in"] is None and amount > 0: + parsed_result["token_in"] = mint + parsed_result["amount_in"] = amount + elif parsed_result["token_out"] is None: + parsed_result["token_out"] = mint + parsed_result["amount_out"] = amount + + # Calculate USD values if token is USDC + if parsed_result["token_in"] == "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": + parsed_result["amount_in_USD"] = parsed_result["amount_in"] + if parsed_result["token_out"] == "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": + parsed_result["amount_out_USD"] = parsed_result["amount_out"] + + # Calculate percentage swapped + if parsed_result["amount_in"] > 0 and parsed_result["amount_out"] > 0: + parsed_result["percentage_swapped"] = (parsed_result["amount_out"] / parsed_result["amount_in"]) * 100 + + return parsed_result + + except Exception as e: + logging.error(f"Error fetching transaction details: {e}") + + return None + + + async def get_transaction_details_rpc(tx_signature, readfromDump=False): url = SOLANA_HTTP_URL @@ -407,12 +481,59 @@ async def get_transaction_details_rpc(tx_signature, readfromDump=False): with open('./logs/transation_details.json', 'w') as f: json.dump(transaction_details, f, indent=2) - if 'result' in transaction_details: - #print(transaction_details['result']) - return transaction_details['result'] + result = transaction_details['result'] + + # Initialize default result structure + parsed_result = { + "order_id": None, + "token_in": None, + "token_out": None, + "amount_in": 0, + "amount_out": 0, + "amount_in_USD": 0, + "amount_out_USD": 0, + "percentage_swapped": 0 + } + + # Extract order_id from logs + log_messages = result.get("meta", {}).get("logMessages", []) + for log in log_messages: + if "order_id" in log: + parsed_result["order_id"] = log.split(":")[1].strip() + break + + # Extract token transfers from innerInstructions + inner_instructions = result.get('meta', {}).get('innerInstructions', []) + for instruction_set in inner_instructions: + for instruction in instruction_set.get('instructions', []): + if instruction.get('program') == 'spl-token' and instruction.get('parsed', {}).get('type') == 'transferChecked': + info = instruction['parsed']['info'] + mint = info['mint'] + amount = float(info['tokenAmount']['amount']) / 10 ** info['tokenAmount']['decimals'] # Adjust for decimals + + # Determine which token is being swapped in and out based on zero balances + if parsed_result["token_in"] is None and amount > 0: + parsed_result["token_in"] = mint + parsed_result["amount_in"] = amount + elif parsed_result["token_out"] is None: + parsed_result["token_out"] = mint + parsed_result["amount_out"] = amount + + # Calculate USD values if token is USDC + if parsed_result["token_in"] == "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": + parsed_result["amount_in_USD"] = parsed_result["amount_in"] + if parsed_result["token_out"] == "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": + parsed_result["amount_out_USD"] = parsed_result["amount_out"] + + # Calculate percentage swapped + if parsed_result["amount_in"] > 0 and parsed_result["amount_out"] > 0: + parsed_result["percentage_swapped"] = (parsed_result["amount_out"] / parsed_result["amount_in"]) * 100 + + return parsed_result else: print("Unexpected response:", transaction_details) + return None except requests.exceptions.RequestException as e: print("Error fetching transaction details:", e) @@ -474,6 +595,7 @@ async def process_log(log_result): # "Program log: before_source_balance: 58730110139, before_destination_balance: 202377778, amount_in: 58730110139, expect_amount_out: 270109505, min_return: 267408410", # "Program log: after_source_balance: 0, after_destination_balance: 472509072", # "Program log: source_token_change: 58730110139, destination_token_change: 270131294", + async def parse_swap_logs(tx_signature_str: str, logs: List[str]) -> Dict[str, Any]: token_in = None token_out = None @@ -481,6 +603,11 @@ async def parse_swap_logs(tx_signature_str: str, logs: List[str]) -> Dict[str, A amount_out = 0 order_id = None + try: + token_in, token_out, amount_in, amount_out = await get_swap_transaction_details(tx_signature_str) + except Exception as e: + logging.error(f"Error fetching swap transaction details: {e}") + transaction_details = await get_transaction_details_rpc(tx_signature_str) token_in, token_out, amount_in, amount_out = _extract_token_info(transaction_details) @@ -508,26 +635,7 @@ async def parse_swap_logs(tx_signature_str: str, logs: List[str]) -> Dict[str, A "percentage_swapped": percentage_swapped } -def _extract_token_info(transaction_details: Dict[str, Any]) -> Tuple[str, str, float, float]: - inner_instructions = transaction_details.get('meta', {}).get('innerInstructions', []) - token_in = None - token_out = None - amount_in = 0 - amount_out = 0 - for instruction_set in inner_instructions: - for instruction in instruction_set.get('instructions', []): - if 'parsed' in instruction and 'info' in instruction['parsed']: - info = instruction['parsed']['info'] - if info.get('type') == 'transfer': - if token_in is None: - token_in = info.get('source') - amount_in = float(info.get('amount', 0)) - else: - token_out = info.get('destination') - amount_out = float(info.get('amount', 0)) - - return token_in, token_out, amount_in, amount_out def _get_pre_balance(transaction_details: Dict[str, Any], token: str) -> float: pre_balances = transaction_details.get('meta', {}).get('preTokenBalances', []) @@ -694,7 +802,7 @@ async def main(): # Initialize logging logging.basicConfig(level=logging.DEBUG) await send_telegram_message("Solana Agent Started. Connecting to mainnet...") - #await subscribe_to_wallet() + await subscribe_to_wallet() def run_flask(): # Run Flask app without the reloader, so we can run the async main function From d6551660c4843e5d7b763b1855aa6fd092187915 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Mon, 7 Oct 2024 02:14:07 +0300 Subject: [PATCH 16/21] parsing tr details succeeded !!!! --- crypto/sol/app.py | 113 +++++++++++++++++++++++++++++++++------------- 1 file changed, 82 insertions(+), 31 deletions(-) diff --git a/crypto/sol/app.py b/crypto/sol/app.py index 8f39c2d..f8d94fe 100644 --- a/crypto/sol/app.py +++ b/crypto/sol/app.py @@ -269,7 +269,7 @@ async def get_token_balance_rpc(wallet_address, token_address): async def get_token_name(mint_address): try: - token_info = await solana_client.get_token_supply(Pubkey.from_string(mint_address)) + token_info = await solana_client.get_account_info_json_parsed(Pubkey.from_string(mint_address)) if token_info.value and 'symbol' in token_info.value: return token_info.value['symbol'] except Exception as e: @@ -296,7 +296,8 @@ async def get_wallet_balances(wallet_address): info = parsed_data['info'] if isinstance(info, dict) and 'mint' in info and 'tokenAmount' in info: mint = info['mint'] - amount = float(info['tokenAmount']['uiAmount']) + #amount = float(info['tokenAmount']['amount']) / (10 ** info['tokenAmount']['decimals']) + amount = float(info['tokenAmount']['amount'])/10**info['tokenAmount']['decimals'] if amount > 0: token_name = await get_token_name(mint) or 'Unknown' balances[mint] = { @@ -500,7 +501,7 @@ async def get_transaction_details_rpc(tx_signature, readfromDump=False): log_messages = result.get("meta", {}).get("logMessages", []) for log in log_messages: if "order_id" in log: - parsed_result["order_id"] = log.split(":")[1].strip() + parsed_result["order_id"] = log.split(":")[2].strip() break # Extract token transfers from innerInstructions @@ -516,15 +517,9 @@ async def get_transaction_details_rpc(tx_signature, readfromDump=False): if parsed_result["token_in"] is None and amount > 0: parsed_result["token_in"] = mint parsed_result["amount_in"] = amount - elif parsed_result["token_out"] is None: - parsed_result["token_out"] = mint - parsed_result["amount_out"] = amount + # Calculate USD values if token is USDC - if parsed_result["token_in"] == "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": - parsed_result["amount_in_USD"] = parsed_result["amount_in"] - if parsed_result["token_out"] == "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": - parsed_result["amount_out_USD"] = parsed_result["amount_out"] # Calculate percentage swapped if parsed_result["amount_in"] > 0 and parsed_result["amount_out"] > 0: @@ -555,33 +550,87 @@ async def process_log(log_result): if log_result['value']['err']: return - logs = log_result['value']['logs'] try: # Detect swap operations in logs swap_operations = ['Program log: Instruction: Swap', 'Program log: Instruction: Swap2'] - for log_entry in logs: - if any(op in log_entry for op in swap_operations): - try: - tx_signature_str = log_result['value']['signature'] - details = await parse_swap_logs(tx_signature_str, logs) - - message_text = ( - f"Swap detected:\n" - f"Order ID: {details['order_id']}\n" - f"Token In: {details['token_in']}\n" - f"Token Out: {details['token_out']}\n" - f"Amount In USD: {details['amount_in_USD']}\n" - f"Percentage Swapped: {details['percentage_swapped']:.2f}%" - ) + if any(op in logs for op in swap_operations): + # Save the log to a file + await save_log(log_result) + tx_signature_str = log_result['value']['signature'] - await send_telegram_message(message_text) - await follow_move(details) + before_source_balance = 0 + source_token_change = 0 + + tr_details = { + "order_id": None, + "token_in": None, + "token_out": None, + "amount_in": 0, + "amount_out": 0, + "amount_in_USD": 0, + "amount_out_USD": 0, + "percentage_swapped": 0 + } + i = 0 + while i < len(logs): + log_entry = logs[i] - except Exception as e: - logging.error(f"Error fetching transaction details: {e}") - return + # Check if we found the 'order_id' + if tr_details["order_id"] is None and "order_id" in log_entry: + # Extract the order_id + tr_details["order_id"] = log_entry.split(":")[-1].strip() + tr_details["token_in"] = logs[i + 1].split(":")[-1].strip() + tr_details["token_out"] = logs[i + 2].split(":")[-1].strip() + + # Look for the token change amounts after tokens have been found + if "source_token_change" in log_entry: + parts = log_entry.split(", ") + for part in parts: + if "source_token_change" in part: + tr_details["amount_in"] = float(part.split(":")[-1].strip()) / 10 ** 6 # Assuming 6 decimals + elif "destination_token_change" in part: + tr_details["amount_out"] = float(part.split(":")[-1].strip()) / 10 ** 6 # Assuming 6 decimals + + i += 1 + + # calculatte percentage swapped by digging before_source_balance, source_token_change and after_source_balance + + # "Program log: before_source_balance: 19471871, before_destination_balance: 0, amount_in: 19471871, expect_amount_out: 770877527, min_return: 763168752", + # "Program log: after_source_balance: 0, after_destination_balance: 770570049", + # "Program log: source_token_change: 19471871, destination_token_change: 770570049", + if "before_source_balance" in log_entry: + parts = log_entry.split(", ") + for part in parts: + if "before_source_balance" in part: + before_source_balance = float(part.split(":")[-1].strip()) / 10 ** 6 + if "source_token_change" in log_entry: + parts = log_entry.split(", ") + for part in parts: + if "source_token_change" in part: + source_token_change = float(part.split(":")[-1].strip()) / 10 ** 6 + + + + if tr_details["order_id"] is None or tr_details["token_in"] is None or tr_details["token_out"] is None or tr_details["amount_in"] == 0 or tr_details["amount_out"] == 0: + details = await parse_swap_logs(tx_signature_str, logs) + + if before_source_balance > 0 and source_token_change > 0: + tr_details["percentage_swapped"] = (source_token_change / before_source_balance) * 100 + + if tr_details["order_id"] is not None : + message_text = ( + f"Swap detected:\n" + f"Order ID: {tr_details['order_id']}\n" + f"Token In: {tr_details['token_in']}\n" + f"Token Out: {tr_details['token_out']}\n" + f"Amount In USD: {tr_details['amount_in_USD']}\n" + f"Percentage Swapped: {tr_details['percentage_swapped']:.2f}%" + ) + + await send_telegram_message(message_text) + await follow_move(tr_details) except Exception as e: logging.error(f"Error processing log: {e}") @@ -664,6 +713,7 @@ async def follow_move(move): your_balance = your_balance_info['amount'] token_name = your_balance_info['name'] + # move["percentage_swapped"] = (move["amount_out"] / move["amount_in"]) * 100 # Calculate the amount to swap based on the same percentage as the followed move amount_to_swap = your_balance * (move['percentage_swapped'] / 100) @@ -709,13 +759,14 @@ async def follow_move(move): ) logging.warning(message) await send_telegram_message(message) + + # Helper functions (implement these according to your needs) async def on_logs(log): logging.debug(f"Received log: {log}") - await save_log(log) await process_log(log) From a6f43b8a9ae928abeedfcbbd1577bb158a5ab403 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Mon, 7 Oct 2024 10:39:54 +0300 Subject: [PATCH 17/21] work on follow_move --- crypto/sol/app.py | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/crypto/sol/app.py b/crypto/sol/app.py index f8d94fe..1cc2400 100644 --- a/crypto/sol/app.py +++ b/crypto/sol/app.py @@ -614,7 +614,8 @@ async def process_log(log_result): if tr_details["order_id"] is None or tr_details["token_in"] is None or tr_details["token_out"] is None or tr_details["amount_in"] == 0 or tr_details["amount_out"] == 0: - details = await parse_swap_logs(tx_signature_str, logs) + logging.warning("Incomplete swap details found in logs. Getting details from transaction") + details = await get_transaction_details_info(tx_signature_str, logs) if before_source_balance > 0 and source_token_change > 0: tr_details["percentage_swapped"] = (source_token_change / before_source_balance) * 100 @@ -645,7 +646,7 @@ async def process_log(log_result): # "Program log: after_source_balance: 0, after_destination_balance: 472509072", # "Program log: source_token_change: 58730110139, destination_token_change: 270131294", -async def parse_swap_logs(tx_signature_str: str, logs: List[str]) -> Dict[str, Any]: +async def get_transaction_details_info(tx_signature_str: str, logs: List[str]) -> Dict[str, Any]: token_in = None token_out = None amount_in = 0 @@ -730,24 +731,38 @@ async def follow_move(move): slippage_bps=1, ) - raw_transaction = VersionedTransaction.from_bytes(base64.b64decode(transaction_data)) - signature = private_key.sign_message(message.to_bytes_versioned(raw_transaction.message)) - signed_txn = VersionedTransaction.populate(raw_transaction.message, [signature]) - opts = TxOpts(skip_preflight=False, preflight_commitment=Processed) - result = await async_client.send_raw_transaction(txn=bytes(signed_txn), opts=opts) - transaction_id = json.loads(result.to_json())['result'] + + instructions = [transaction_data['instruction']] # Adjust as needed + message = Message(instructions, private_key.pubkey()) + blockhash = await async_client.get_latest_blockhash() + tx = Transaction([private_key], message, blockhash.value) + + result = await async_client.send_transaction(tx, private_key) + transaction_id = result.value output_token_info = your_balances.get(move['token_out'], {'name': 'Unknown'}) output_token_name = output_token_info['name'] - message = ( + # raw_transaction = VersionedTransaction.from_bytes(base64.b64decode(transaction_data)) + # tx_message = raw_transaction.message + # signature = private_key.sign_message(tx_message.to_bytes_versioned()) + # signed_txn = VersionedTransaction.populate(tx_message, [signature]) + # opts = TxOpts(skip_preflight=False, preflight_commitment=Processed) + # result = await async_client.send_raw_transaction(txn=bytes(signed_txn), opts=opts) + # transaction_id = json.loads(result.to_json())['result'] + + # output_token_info = your_balances.get(move['token_out'], {'name': 'Unknown'}) + # output_token_name = output_token_info['name'] + + notification = ( f"Move Followed:\n" f"Swapped {amount_to_swap:.6f} {token_name} ({move['token_in']}) " f"(same {move['percentage_swapped']:.2f}% as followed wallet)\n" f"for {transaction_data['outputAmount'] / 1e6:.6f} {output_token_name} ({move['token_out']})" ) - logging.info(message) - await send_telegram_message(message) + logging.info(notification) + await send_telegram_message(notification) + except Exception as e: error_message = f"Swap Error:\n{str(e)}" logging.error(error_message) From a6ca2e3886cb9886b8501c54cbc7c5d09d5c96c3 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Mon, 7 Oct 2024 12:56:32 +0300 Subject: [PATCH 18/21] succesfully parsing swaps ! follow move has all the needed parameters now. --- crypto/sol/app.py | 362 ++++++++++++++++++++++++++-------------------- 1 file changed, 207 insertions(+), 155 deletions(-) diff --git a/crypto/sol/app.py b/crypto/sol/app.py index 1cc2400..25739d7 100644 --- a/crypto/sol/app.py +++ b/crypto/sol/app.py @@ -6,7 +6,7 @@ from solana.rpc.async_api import AsyncClient from solana.transaction import Signature from solana.rpc.websocket_api import connect from solana.rpc.types import TokenAccountOpts, TxOpts -from solana.rpc.commitment import Confirmed +from solana.rpc.commitment import Confirmed, Processed from solana.transaction import Transaction from base64 import b64decode import base58 @@ -20,6 +20,7 @@ from solders.message import Message from solders.instruction import Instruction from solders.hash import Hash from solders.instruction import CompiledInstruction +from solders import message from jupiter_python_sdk.jupiter import Jupiter, Jupiter_DCA from dexscreener import DexscreenerClient from telegram import Bot @@ -77,12 +78,13 @@ def retry_last_log(): with open(latest_log_file, 'r') as f: log = json.load(f) + # await process_log(log) # Run the asynchronous process_log function asyncio.run(process_log(log)) - return jsonify({"status": "Log processed successfully"}), 200 + return jsonify({"status": "Log dump processed successfully"}), 200 except Exception as e: - logging.error(f"Error processing log: {e}") + logging.error(f"Error processing log dump: {e}") return jsonify({"error": "Failed to process log"}), 500 @@ -202,18 +204,6 @@ async def get_sol_price_from_dexscreener() -> float: prices = await get_prices_from_dexscreener([sol_address]) return prices.get(sol_address, 0.0) -async def get_sol_price() -> float: - url = f"https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies={DISPLAY_CURRENCY.lower()}" - async with aiohttp.ClientSession() as session: - async with session.get(url) as response: - if response.status == 200: - data = await response.json() - return data['solana'][DISPLAY_CURRENCY.lower()] - else: - logging.error(f"Failed to get SOL price. Status: {response.status}") - return None - - async def get_token_balance_rpc(wallet_address, token_address): url = SOLANA_HTTP_URL headers = {"Content-Type": "application/json"} @@ -324,32 +314,33 @@ async def get_wallet_balances(wallet_address): return balances -async def convert_balances_to_currency(balances, token_prices, sol_price): +async def convert_balances_to_currency(balances , sol_price): converted_balances = {} for address, info in balances.items(): converted_balance = info.copy() # Create a copy of the original info if info['name'] == 'SOL': converted_balance['value'] = info['amount'] * sol_price - elif address in token_prices: - converted_balance['value'] = info['amount'] * token_prices[address] + elif address in TOKEN_PRICES: + converted_balance['value'] = info['amount'] * TOKEN_PRICES[address] else: converted_balance['value'] = None # Price not available logging.warning(f"Price not available for token {info['name']} ({address})") converted_balances[address] = converted_balance return converted_balances + async def list_initial_wallet_states(): - global TOKEN_ADDRESSES, FOLLOWED_WALLET_VALUE, YOUR_WALLET_VALUE + global TOKEN_ADDRESSES, FOLLOWED_WALLET_VALUE, YOUR_WALLET_VALUE, TOKEN_PRICES followed_wallet_balances = await get_wallet_balances(FOLLOWED_WALLET) your_wallet_balances = await get_wallet_balances(YOUR_WALLET) all_token_addresses = list(set(followed_wallet_balances.keys()) | set(your_wallet_balances.keys())) - token_prices = await get_token_prices(all_token_addresses) + TOKEN_PRICES = await get_token_prices(all_token_addresses) sol_price = await get_sol_price() - followed_converted_balances = await convert_balances_to_currency(followed_wallet_balances, token_prices, sol_price) - your_converted_balances = await convert_balances_to_currency(your_wallet_balances, token_prices, sol_price) + followed_converted_balances = await convert_balances_to_currency(followed_wallet_balances, sol_price) + your_converted_balances = await convert_balances_to_currency(your_wallet_balances, sol_price) TOKEN_ADDRESSES = {address: info for address, info in {**followed_converted_balances, **your_converted_balances}.items() if info['value'] is not None and info['value'] > 0} logging.info(f"Monitoring balances for tokens: {[info['name'] for info in TOKEN_ADDRESSES.values()]}") @@ -386,20 +377,6 @@ async def list_initial_wallet_states(): async def get_swap_transaction_details(tx_signature_str): t = await solana_client.get_transaction(Signature.from_string(tx_signature_str), max_supported_transaction_version=0) try: - transaction = t.value.transaction - message = transaction.transaction.message - instructions = message.instructions - - accounts = t.value.transaction.transaction.message.instructions[0].accounts - instructions = t.value.transaction.transaction.message.instructions - - # Assume the swap is the first instruction - swap_instruction = instructions[0] - - # Extract accounts involved in the swap instruction - accounts = swap_instruction.accounts - - # Initialize result dictionary parsed_result = { "order_id": None, "token_in": None, @@ -418,6 +395,7 @@ async def get_swap_transaction_details(tx_signature_str): # parsed_result["order_id"] = log.split(":")[1].strip() # break + instructions = t.value.transaction.transaction.message.instructions # Parse the swap instruction to extract token addresses, amounts, and types for instruction in instructions: if isinstance(instruction, CompiledInstruction): @@ -455,83 +433,156 @@ async def get_swap_transaction_details(tx_signature_str): async def get_transaction_details_rpc(tx_signature, readfromDump=False): - url = SOLANA_HTTP_URL - # url = 'https://solana.drpc.org' - headers = {"Content-Type": "application/json"} - data = { - "jsonrpc": "2.0", - "id": 1, - "method": "getTransaction", - "params": [ - tx_signature, - { - "encoding": "jsonParsed", - "maxSupportedTransactionVersion": 0 - } - ] - } + try: if readfromDump and os.path.exists('./logs/transation_details.json'): with open('./logs/transation_details.json', 'r') as f: # trump_swap_tr_details transaction_details = json.load(f) return transaction_details else: - response = requests.post(url, headers=headers, data=json.dumps(data)) - response.raise_for_status() # Raises an error for bad responses - transaction_details = response.json() + transaction_details = await solana_jsonrpc("getTransaction", [tx_signature]) with open('./logs/transation_details.json', 'w') as f: json.dump(transaction_details, f, indent=2) - if 'result' in transaction_details: - result = transaction_details['result'] + # Initialize default result structure + parsed_result = { + "order_id": None, + "token_in": None, + "token_out": None, + "amount_in": 0, + "amount_out": 0, + "amount_in_USD": 0, + "amount_out_USD": 0, + "percentage_swapped": 0 + } - # Initialize default result structure - parsed_result = { - "order_id": None, - "token_in": None, - "token_out": None, - "amount_in": 0, - "amount_out": 0, - "amount_in_USD": 0, - "amount_out_USD": 0, - "percentage_swapped": 0 - } + # Extract order_id from logs + log_messages = transaction_details.get("meta", {}).get("logMessages", []) + for log in log_messages: + if "order_id" in log: + parsed_result["order_id"] = log.split(":")[2].strip() + break - # Extract order_id from logs - log_messages = result.get("meta", {}).get("logMessages", []) - for log in log_messages: - if "order_id" in log: - parsed_result["order_id"] = log.split(":")[2].strip() - break + # Extract token transfers from innerInstructions + inner_instructions = transaction_details.get('meta', {}).get('innerInstructions', []) + for instruction_set in inner_instructions: + for instruction in instruction_set.get('instructions', []): + if instruction.get('program') == 'spl-token' and instruction.get('parsed', {}).get('type') == 'transferChecked': + info = instruction['parsed']['info'] + mint = info['mint'] + amount = float(info['tokenAmount']['amount']) / 10 ** info['tokenAmount']['decimals'] # Adjust for decimals + + # Determine which token is being swapped in and out based on zero balances + if parsed_result["token_in"] is None and amount > 0: + parsed_result["token_in"] = mint + parsed_result["amount_in"] = amount + + + if parsed_result["token_in"] is None or parsed_result["token_out"] is None: + # if we've failed to extract token_in and token_out from the transaction details, try a second method + inner_instructions = transaction_details.get('meta', {}).get('innerInstructions', []) + transfers = [] - # Extract token transfers from innerInstructions - inner_instructions = result.get('meta', {}).get('innerInstructions', []) for instruction_set in inner_instructions: for instruction in instruction_set.get('instructions', []): - if instruction.get('program') == 'spl-token' and instruction.get('parsed', {}).get('type') == 'transferChecked': + if instruction.get('program') == 'spl-token' and instruction.get('parsed', {}).get('type') in ['transfer', 'transferChecked']: info = instruction['parsed']['info'] - mint = info['mint'] - amount = float(info['tokenAmount']['amount']) / 10 ** info['tokenAmount']['decimals'] # Adjust for decimals - - # Determine which token is being swapped in and out based on zero balances - if parsed_result["token_in"] is None and amount > 0: - parsed_result["token_in"] = mint - parsed_result["amount_in"] = amount + amount = float(info['amount']) if 'amount' in info else float(info['tokenAmount']['amount']) + decimals = info['tokenAmount']['decimals'] if 'tokenAmount' in info else 0 + adjusted_amount = amount / (10 ** decimals) + transfers.append({ + 'mint': info.get('mint'), + 'amount': adjusted_amount, + 'source': info['source'], + 'destination': info['destination'] + }) + + # Identify token_in and token_out + if len(transfers) >= 2: + parsed_result["token_in"] = transfers[0]['mint'] + parsed_result["amount_in"] = transfers[0]['amount'] + parsed_result["token_out"] = transfers[-1]['mint'] + parsed_result["amount_out"] = transfers[-1]['amount'] + # If mint is not provided, query the Solana network for the account data + if parsed_result["token_in"] is None or parsed_result["token_out"] is None: + #for transfer in transfers: + # do only first and last transfer + for transfer in [transfers[0], transfers[-1]]: + if transfer['mint'] is None: + # Query the Solana network for the account data + account_data_result = await solana_jsonrpc("getAccountInfo", [transfer['source']]) - # Calculate USD values if token is USDC + if 'value' in account_data_result and 'data' in account_data_result['value']: + account_data_value = account_data_result['value'] + account_data_data = account_data_value['data'] + if 'parsed' in account_data_data and 'info' in account_data_data['parsed']: + account_data_info = account_data_data['parsed']['info'] + if 'mint' in account_data_info: + transfer['mint'] = account_data_info['mint'] + if parsed_result["token_in"] is None: + parsed_result["token_in"] = transfer['mint'] + parsed_result["amount_in"] = transfer['amount']/10**6 + elif parsed_result["token_out"] is None: + parsed_result["token_out"] = transfer['mint'] + parsed_result["amount_out"] = transfer['amount']/10**6 - # Calculate percentage swapped - if parsed_result["amount_in"] > 0 and parsed_result["amount_out"] > 0: - parsed_result["percentage_swapped"] = (parsed_result["amount_out"] / parsed_result["amount_in"]) * 100 + pre_balalnces = transaction_details.get('meta', {}).get('preTokenBalances', []) + for balance in pre_balalnces: + if balance['mint'] == parsed_result["token_in"] and balance['owner'] == FOLLOWED_WALLET: + parsed_result["before_source_balance"] = float(balance['uiTokenAmount']['amount']) / 10 ** balance['uiTokenAmount']['decimals'] + break + + + # Calculate percentage swapped + if parsed_result["amount_in"] > 0 and parsed_result["before_source_balance"] > 0: + parsed_result["percentage_swapped"] = (parsed_result["amount_in"] / parsed_result["before_source_balance"]) * 100 + + return parsed_result - return parsed_result - else: - print("Unexpected response:", transaction_details) - return None except requests.exceptions.RequestException as e: print("Error fetching transaction details:", e) + +async def solana_jsonrpc(method, params = None, jsonParsed = True): + # target json example: + # data = { + # "jsonrpc": "2.0", + # "id": 1, + # "method": "getTransaction", + # "params": [ + # tx_signature, + # { + # "encoding": "jsonParsed", + # "maxSupportedTransactionVersion": 0 + # } + # ] + # } + + data = { + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params or [] + } + data["params"].append({"maxSupportedTransactionVersion": 0}) + if jsonParsed: + data["params"][1]["encoding"] = "jsonParsed" + + + try: + # url = 'https://solana.drpc.org' + response = requests.post(SOLANA_HTTP_URL, headers={"Content-Type": "application/json"}, data=json.dumps(data)) + response.raise_for_status() # Raises an error for bad responses + result = response.json() + if not 'result' in result or 'error' in result: + print("Error fetching data from Solana RPC:", result) + return None + return result['result'] + except Exception as e: + logging.error(f"Error fetching data from Solana RPC: {e}") + return None + async def save_log(log): try: @@ -612,23 +663,23 @@ async def process_log(log_result): source_token_change = float(part.split(":")[-1].strip()) / 10 ** 6 - - if tr_details["order_id"] is None or tr_details["token_in"] is None or tr_details["token_out"] is None or tr_details["amount_in"] == 0 or tr_details["amount_out"] == 0: + # GET DETAILS FROM TRANSACTION IF NOT FOUND IN LOGS + if tr_details["token_in"] is None or tr_details["token_out"] is None or tr_details["amount_in"] == 0 or tr_details["amount_out"] == 0: logging.warning("Incomplete swap details found in logs. Getting details from transaction") - details = await get_transaction_details_info(tx_signature_str, logs) - + tr_details = await get_transaction_details_info(tx_signature_str, logs) + # onlt needed if no details got if before_source_balance > 0 and source_token_change > 0: tr_details["percentage_swapped"] = (source_token_change / before_source_balance) * 100 - if tr_details["order_id"] is not None : - message_text = ( - f"Swap detected:\n" - f"Order ID: {tr_details['order_id']}\n" - f"Token In: {tr_details['token_in']}\n" - f"Token Out: {tr_details['token_out']}\n" - f"Amount In USD: {tr_details['amount_in_USD']}\n" - f"Percentage Swapped: {tr_details['percentage_swapped']:.2f}%" - ) + + message_text = ( + f"Swap detected:\n" + f"Order ID: {tr_details['order_id']}\n" + f"Token In: {tr_details['token_in']}\n" + f"Token Out: {tr_details['token_out']}\n" + f"Amount In USD: {tr_details['amount_in_USD']}\n" + f"Percentage Swapped: {tr_details['percentage_swapped']:.2f}%" + ) await send_telegram_message(message_text) await follow_move(tr_details) @@ -647,52 +698,25 @@ async def process_log(log_result): # "Program log: source_token_change: 58730110139, destination_token_change: 270131294", async def get_transaction_details_info(tx_signature_str: str, logs: List[str]) -> Dict[str, Any]: - token_in = None - token_out = None - amount_in = 0 - amount_out = 0 - order_id = None - + try: - token_in, token_out, amount_in, amount_out = await get_swap_transaction_details(tx_signature_str) + tr_info = await get_swap_transaction_details(tx_signature_str) except Exception as e: logging.error(f"Error fetching swap transaction details: {e}") - transaction_details = await get_transaction_details_rpc(tx_signature_str) - token_in, token_out, amount_in, amount_out = _extract_token_info(transaction_details) + tr_info = await get_transaction_details_rpc(tx_signature_str) # Fetch token prices - token_prices = await get_token_prices([token_in, token_out]) + token_prices = await get_token_prices([tr_info['token_in'], tr_info['token_out']]) # Calculate USD values - amount_in_usd = amount_in * token_prices.get(token_in, 0) - amount_out_usd = amount_out * token_prices.get(token_out, 0) - - # Get the pre-balance of the input token - before_source_balance = _get_pre_balance(transaction_details, token_in) + tr_info['amount_in_usd'] = tr_info['amount_in'] * token_prices.get(tr_info['token_in'], 0) + tr_info['amount_out_usd'] = tr_info['amount_out'] * token_prices.get(tr_info['token_out'], 0) # Calculate the percentage of the source balance that was swapped - percentage_swapped = (amount_in / before_source_balance) * 100 if before_source_balance > 0 else 0 + tr_info['percentage_swapped'] = (tr_info['amount_in'] / tr_info['before_source_balance']) * 100 if tr_info['before_source_balance'] > 0 else 50 + return tr_info - return { - "order_id": order_id, - "token_in": token_in, - "token_out": token_out, - "amount_in": amount_in, - "amount_out": amount_out, - "amount_in_USD": amount_in_usd, - "amount_out_USD": amount_out_usd, - "percentage_swapped": percentage_swapped - } - - - -def _get_pre_balance(transaction_details: Dict[str, Any], token: str) -> float: - pre_balances = transaction_details.get('meta', {}).get('preTokenBalances', []) - for balance in pre_balances: - if balance['mint'] == token: - return float(balance['uiTokenAmount']['amount']) - return 0.0 def _get_pre_balance(transaction_details: Dict[str, Any], token: str) -> float: pre_balances = transaction_details.get('meta', {}).get('preTokenBalances', []) for balance in pre_balances: @@ -706,9 +730,9 @@ async def follow_move(move): your_balance_info = next((balance for balance in your_balances.values() if balance['address'] == move['token_in']), None) if not your_balance_info: - message = f"Move Failed:\nNo balance found for token {move['token_in']}" - logging.warning(message) - await send_telegram_message(message) + msg = f"Move Failed:\nNo balance found for token {move['token_in']}" + logging.warning(msg) + await send_telegram_message(msg) return your_balance = your_balance_info['amount'] @@ -732,17 +756,45 @@ async def follow_move(move): ) - instructions = [transaction_data['instruction']] # Adjust as needed - message = Message(instructions, private_key.pubkey()) - blockhash = await async_client.get_latest_blockhash() - tx = Transaction([private_key], message, blockhash.value) + raw_transaction = VersionedTransaction.from_bytes(base64.b64decode(transaction_data)) + signature = private_key.sign_message(message.to_bytes_versioned(raw_transaction.message)) + signed_txn = VersionedTransaction.populate(raw_transaction.message, [signature]) + opts = TxOpts(skip_preflight=False, preflight_commitment=Processed) + result = await async_client.send_raw_transaction(txn=bytes(signed_txn), opts=opts) + transaction_id = json.loads(result.to_json())['result'] + print(f"Transaction sent: https://explorer.solana.com/tx/{transaction_id}") - result = await async_client.send_transaction(tx, private_key) - transaction_id = result.value + + + # # transaction_data is already a string, no need to json.loads() + # raw_transaction = base64.b64decode(transaction_data) + # # Send the raw transaction + # opts = TxOpts(skip_preflight=False, preflight_commitment=Processed) + # result = await async_client.send_raw_transaction(txn=raw_transaction, opts=opts) + # transaction_id = result.value # This should be the transaction signature + + # Fetch the transaction details to get the output amount + tx_details = await async_client.get_transaction(transaction_id) + output_amount = tx_details.value.meta.post_balances[1] - tx_details.value.meta.pre_balances[1] output_token_info = your_balances.get(move['token_out'], {'name': 'Unknown'}) output_token_name = output_token_info['name'] + + + # transaction_data = json.loads(transaction_data) + + # instructions = [transaction_data['instruction']] # Adjust as needed + # message = Message(instructions, private_key.pubkey()) + # blockhash = await async_client.get_latest_blockhash() + # tx = Transaction([private_key], message, blockhash.value) + + # result = await async_client.send_transaction(tx, private_key) + # transaction_id = result.value + + # output_token_info = your_balances.get(move['token_out'], {'name': 'Unknown'}) + # output_token_name = output_token_info['name'] + # raw_transaction = VersionedTransaction.from_bytes(base64.b64decode(transaction_data)) # tx_message = raw_transaction.message # signature = private_key.sign_message(tx_message.to_bytes_versioned()) @@ -764,16 +816,16 @@ async def follow_move(move): await send_telegram_message(notification) except Exception as e: - error_message = f"Swap Error:\n{str(e)}" + error_message = f"Swap Follow Error:\n{str(e)}" logging.error(error_message) - await send_telegram_message(error_message) + # await send_telegram_message(error_message) else: - message = ( - f"Move Failed:\n" + msg = ( + f"Move Not Followed:\n" f"Insufficient balance to swap {amount_to_swap:.6f} {token_name} ({move['token_in']})" ) - logging.warning(message) - await send_telegram_message(message) + logging.warning(msg) + await send_telegram_message(msg) # Helper functions (implement these according to your needs) @@ -868,7 +920,7 @@ async def main(): # Initialize logging logging.basicConfig(level=logging.DEBUG) await send_telegram_message("Solana Agent Started. Connecting to mainnet...") - await subscribe_to_wallet() + # await subscribe_to_wallet() def run_flask(): # Run Flask app without the reloader, so we can run the async main function From e32901d228817b1a8ed28858a99b339d28ae7128 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Mon, 7 Oct 2024 13:28:23 +0300 Subject: [PATCH 19/21] SWAP FOLLOW WORKS !!! --- crypto/sol/app.py | 54 +++++++++++------------------------------------ 1 file changed, 12 insertions(+), 42 deletions(-) diff --git a/crypto/sol/app.py b/crypto/sol/app.py index 25739d7..d5d03a1 100644 --- a/crypto/sol/app.py +++ b/crypto/sol/app.py @@ -119,6 +119,7 @@ async def send_telegram_message(message): try: await bot.send_message(chat_id=DEVELOPER_CHAT_ID, text=message, parse_mode=ParseMode.HTML) logging.info(f"Telegram message sent: {message}") + # logging.info(f"Telegram message dummy sent: {message}") except Exception as e: logging.error(f"Error sending Telegram message: {str(e)}") @@ -752,7 +753,7 @@ async def follow_move(move): input_mint=move['token_in'], output_mint=move['token_out'], amount=int(amount_to_swap * 1e6), # Convert to lamports - slippage_bps=1, + slippage_bps=50, # Increased to 0.5% ) @@ -760,57 +761,29 @@ async def follow_move(move): signature = private_key.sign_message(message.to_bytes_versioned(raw_transaction.message)) signed_txn = VersionedTransaction.populate(raw_transaction.message, [signature]) opts = TxOpts(skip_preflight=False, preflight_commitment=Processed) + # send the transaction result = await async_client.send_raw_transaction(txn=bytes(signed_txn), opts=opts) + transaction_id = json.loads(result.to_json())['result'] print(f"Transaction sent: https://explorer.solana.com/tx/{transaction_id}") - - - # # transaction_data is already a string, no need to json.loads() - # raw_transaction = base64.b64decode(transaction_data) - # # Send the raw transaction - # opts = TxOpts(skip_preflight=False, preflight_commitment=Processed) - # result = await async_client.send_raw_transaction(txn=raw_transaction, opts=opts) - # transaction_id = result.value # This should be the transaction signature - + # wait for the transaction to be confirmed + # await async_client.wait_for_confirmation(Signature.from_string(transaction_id)) + # wait 10 seconds + await asyncio.sleep(10) # Fetch the transaction details to get the output amount - tx_details = await async_client.get_transaction(transaction_id) + tx_details = await get_transaction_details_rpc(transaction_id) output_amount = tx_details.value.meta.post_balances[1] - tx_details.value.meta.pre_balances[1] output_token_info = your_balances.get(move['token_out'], {'name': 'Unknown'}) output_token_name = output_token_info['name'] - - - # transaction_data = json.loads(transaction_data) - - # instructions = [transaction_data['instruction']] # Adjust as needed - # message = Message(instructions, private_key.pubkey()) - # blockhash = await async_client.get_latest_blockhash() - # tx = Transaction([private_key], message, blockhash.value) - - # result = await async_client.send_transaction(tx, private_key) - # transaction_id = result.value - - # output_token_info = your_balances.get(move['token_out'], {'name': 'Unknown'}) - # output_token_name = output_token_info['name'] - - # raw_transaction = VersionedTransaction.from_bytes(base64.b64decode(transaction_data)) - # tx_message = raw_transaction.message - # signature = private_key.sign_message(tx_message.to_bytes_versioned()) - # signed_txn = VersionedTransaction.populate(tx_message, [signature]) - # opts = TxOpts(skip_preflight=False, preflight_commitment=Processed) - # result = await async_client.send_raw_transaction(txn=bytes(signed_txn), opts=opts) - # transaction_id = json.loads(result.to_json())['result'] - - # output_token_info = your_balances.get(move['token_out'], {'name': 'Unknown'}) - # output_token_name = output_token_info['name'] - notification = ( f"Move Followed:\n" f"Swapped {amount_to_swap:.6f} {token_name} ({move['token_in']}) " f"(same {move['percentage_swapped']:.2f}% as followed wallet)\n" f"for {transaction_data['outputAmount'] / 1e6:.6f} {output_token_name} ({move['token_out']})" + f"\n\nTransaction: {transaction_id}" ) logging.info(notification) await send_telegram_message(notification) @@ -848,16 +821,12 @@ async def subscribe_to_wallet(): reconnect_delay = 5 # Start with a 5-second delay max_reconnect_delay = 60 # Maximum delay of 60 seconds - - await list_initial_wallet_states() - while True: try: async with websockets.connect(uri) as websocket: logger.info("Connected to Solana websocket") subscription_id = await load_subscription_id() - request = { "jsonrpc": "2.0", @@ -920,7 +889,8 @@ async def main(): # Initialize logging logging.basicConfig(level=logging.DEBUG) await send_telegram_message("Solana Agent Started. Connecting to mainnet...") - # await subscribe_to_wallet() + # await list_initial_wallet_states() + await subscribe_to_wallet() def run_flask(): # Run Flask app without the reloader, so we can run the async main function From b7974fbc93f31aadb63b6553bd058d55b5fa6cad Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Mon, 7 Oct 2024 14:25:57 +0300 Subject: [PATCH 20/21] more robust transaction details (with retries) and following --- crypto/sol/app.py | 67 +++++++++++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/crypto/sol/app.py b/crypto/sol/app.py index d5d03a1..a3d1d57 100644 --- a/crypto/sol/app.py +++ b/crypto/sol/app.py @@ -430,7 +430,17 @@ async def get_swap_transaction_details(tx_signature_str): return None - +async def get_transaction_details_with_retry(transaction_id, retry_delay = 9, max_retries = 7): + # wait for the transaction to be confirmed + # await async_client.wait_for_confirmation(Signature.from_string(transaction_id)) + # qwery every 5 seconds for the transaction details untill not None or 30 seconds + for _ in range(max_retries): + tx_details = await get_transaction_details_rpc(transaction_id) + if tx_details is not None: + break + logging.info(f"({_} of {max_retries}) Waiting for transaction details for {transaction_id}") + await asyncio.sleep(retry_delay) + return tx_details async def get_transaction_details_rpc(tx_signature, readfromDump=False): @@ -445,6 +455,10 @@ async def get_transaction_details_rpc(tx_signature, readfromDump=False): with open('./logs/transation_details.json', 'w') as f: json.dump(transaction_details, f, indent=2) + if transaction_details is None: + logging.error(f"Error fetching transaction details for {tx_signature}") + return None + # Initialize default result structure parsed_result = { "order_id": None, @@ -536,8 +550,11 @@ async def get_transaction_details_rpc(tx_signature, readfromDump=False): # Calculate percentage swapped - if parsed_result["amount_in"] > 0 and parsed_result["before_source_balance"] > 0: - parsed_result["percentage_swapped"] = (parsed_result["amount_in"] / parsed_result["before_source_balance"]) * 100 + try: + if parsed_result["amount_in"] > 0 and 'before_source_balance' in parsed_result and parsed_result["before_source_balance"] > 0: + parsed_result["percentage_swapped"] = (parsed_result["amount_in"] / parsed_result["before_source_balance"]) * 100 + except Exception as e: + logging.error(f"Error calculating percentage swapped: {e}") return parsed_result @@ -674,8 +691,7 @@ async def process_log(log_result): message_text = ( - f"Swap detected:\n" - f"Order ID: {tr_details['order_id']}\n" + f"Swap detected:\n" f"Token In: {tr_details['token_in']}\n" f"Token Out: {tr_details['token_out']}\n" f"Amount In USD: {tr_details['amount_in_USD']}\n" @@ -705,7 +721,7 @@ async def get_transaction_details_info(tx_signature_str: str, logs: List[str]) - except Exception as e: logging.error(f"Error fetching swap transaction details: {e}") - tr_info = await get_transaction_details_rpc(tx_signature_str) + tr_info = await get_transaction_details_with_retry(tx_signature_str) # Fetch token prices token_prices = await get_token_prices([tr_info['token_in'], tr_info['token_out']]) @@ -765,26 +781,31 @@ async def follow_move(move): result = await async_client.send_raw_transaction(txn=bytes(signed_txn), opts=opts) transaction_id = json.loads(result.to_json())['result'] - print(f"Transaction sent: https://explorer.solana.com/tx/{transaction_id}") + print(f"Follow Transaction Sent: https://solscan.io/tx/{transaction_id}") - # wait for the transaction to be confirmed - # await async_client.wait_for_confirmation(Signature.from_string(transaction_id)) - # wait 10 seconds - await asyncio.sleep(10) - # Fetch the transaction details to get the output amount - tx_details = await get_transaction_details_rpc(transaction_id) - output_amount = tx_details.value.meta.post_balances[1] - tx_details.value.meta.pre_balances[1] + tx_details = await get_transaction_details_with_retry(transaction_id) - output_token_info = your_balances.get(move['token_out'], {'name': 'Unknown'}) - output_token_name = output_token_info['name'] + if tx_details is None: + logging.info(f"Failed to get transaction details for {transaction_id}") + notification = ( + f"Move Followed:\n" + f"Swapped {amount_to_swap:.6f} {token_name} ({move['token_in']}) " + f"(same {move['percentage_swapped']:.2f}% as followed wallet)\n" + f"\n\nTransaction: {transaction_id}" + ) + + else: + output_token_info = your_balances.get(move['token_out'], {'name': 'Unknown'}) + output_token_name = output_token_info['name'] - notification = ( - f"Move Followed:\n" - f"Swapped {amount_to_swap:.6f} {token_name} ({move['token_in']}) " - f"(same {move['percentage_swapped']:.2f}% as followed wallet)\n" - f"for {transaction_data['outputAmount'] / 1e6:.6f} {output_token_name} ({move['token_out']})" - f"\n\nTransaction: {transaction_id}" - ) + notification = ( + f"Move Followed:\n" + f"Swapped {amount_to_swap:.6f} {token_name} ({move['token_in']}) " + f"(same {move['percentage_swapped']:.2f}% as followed wallet)\n" + f"for {tx_details['amount_out']:.6f} {output_token_name} ({move['token_out']})" + # f"Amount In USD: {tr_details['amount_in_USD']}\n" + f"\n\nTransaction: {transaction_id}" + ) logging.info(notification) await send_telegram_message(notification) From e2b67c88a8501eb3c89b4cf74ba3284acb988df4 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Mon, 7 Oct 2024 17:27:56 +0300 Subject: [PATCH 21/21] quickfix decimals issue (todo fix properly) --- .gitignore | 1 + crypto/sol/app.py | 47 ++++++++++++++++++++++++++++++++++------------- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 2b59296..1b5f1a3 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ agent-mAId/dist/main.exe agent-mAId/output.wav .node-persist/storage/* logs/* +crypto/sol/.env diff --git a/crypto/sol/app.py b/crypto/sol/app.py index a3d1d57..cde34d0 100644 --- a/crypto/sol/app.py +++ b/crypto/sol/app.py @@ -289,12 +289,14 @@ async def get_wallet_balances(wallet_address): mint = info['mint'] #amount = float(info['tokenAmount']['amount']) / (10 ** info['tokenAmount']['decimals']) amount = float(info['tokenAmount']['amount'])/10**info['tokenAmount']['decimals'] + decimals = info['tokenAmount']['decimals'] if amount > 0: token_name = await get_token_name(mint) or 'Unknown' balances[mint] = { 'name': token_name, 'address': mint, - 'amount': amount + 'amount': amount, + 'decimals': decimals } logging.debug(f"Balance for {token_name} ({mint}): {amount}") else: @@ -430,14 +432,17 @@ async def get_swap_transaction_details(tx_signature_str): return None -async def get_transaction_details_with_retry(transaction_id, retry_delay = 9, max_retries = 7): +async def get_transaction_details_with_retry(transaction_id, retry_delay = 11, max_retries = 11): # wait for the transaction to be confirmed # await async_client.wait_for_confirmation(Signature.from_string(transaction_id)) # qwery every 5 seconds for the transaction details untill not None or 30 seconds for _ in range(max_retries): - tx_details = await get_transaction_details_rpc(transaction_id) - if tx_details is not None: - break + try: + tx_details = await get_transaction_details_rpc(transaction_id) + if tx_details is not None: + break + except Exception as e: + logging.error(f"Error fetching transaction details: {e}") logging.info(f"({_} of {max_retries}) Waiting for transaction details for {transaction_id}") await asyncio.sleep(retry_delay) return tx_details @@ -505,6 +510,7 @@ async def get_transaction_details_rpc(tx_signature, readfromDump=False): amount = float(info['amount']) if 'amount' in info else float(info['tokenAmount']['amount']) decimals = info['tokenAmount']['decimals'] if 'tokenAmount' in info else 0 adjusted_amount = amount / (10 ** decimals) + # adjusted_amount = float(info["amount"]) / (10 ** (info["tokenAmount"]["decimals"] if 'tokenAmount' in info else 0)) transfers.append({ 'mint': info.get('mint'), 'amount': adjusted_amount, @@ -622,7 +628,7 @@ async def process_log(log_result): logs = log_result['value']['logs'] try: # Detect swap operations in logs - swap_operations = ['Program log: Instruction: Swap', 'Program log: Instruction: Swap2'] + swap_operations = ['Program log: Instruction: Swap', 'Program log: Instruction: Swap2', 'Program log: Instruction: SwapExactAmountIn'] if any(op in logs for op in swap_operations): # Save the log to a file @@ -688,7 +694,10 @@ async def process_log(log_result): # onlt needed if no details got if before_source_balance > 0 and source_token_change > 0: tr_details["percentage_swapped"] = (source_token_change / before_source_balance) * 100 - + #dirty fix for percentage > 100 (decimals 9 but expecting 6) + if tr_details["percentage_swapped"] > 100: + tr_details["percentage_swapped"] = tr_details["percentage_swapped"] / 1000 + message_text = ( f"Swap detected:\n" @@ -730,7 +739,7 @@ async def get_transaction_details_info(tx_signature_str: str, logs: List[str]) - tr_info['amount_in_usd'] = tr_info['amount_in'] * token_prices.get(tr_info['token_in'], 0) tr_info['amount_out_usd'] = tr_info['amount_out'] * token_prices.get(tr_info['token_out'], 0) - # Calculate the percentage of the source balance that was swapped + # Calculate the percentage of the source balance that was swapped; ToDo: fix decimals for percentage tr_info['percentage_swapped'] = (tr_info['amount_in'] / tr_info['before_source_balance']) * 100 if tr_info['before_source_balance'] > 0 else 50 return tr_info @@ -759,20 +768,24 @@ async def follow_move(move): # Calculate the amount to swap based on the same percentage as the followed move amount_to_swap = your_balance * (move['percentage_swapped'] / 100) + # # always get 99% of the amount to swap + # amount_to_swap = amount_to_swap * 0.99 + if your_balance >= amount_to_swap: try: private_key = Keypair.from_bytes(base58.b58decode(os.getenv("PK"))) async_client = AsyncClient(SOLANA_WS_URL) jupiter = Jupiter(async_client, private_key) - + + # Convert to lamports + # if decimals is 6, then amount = amount * 1e6; if 9, then amount = amount * 1e9 + amount = int(amount_to_swap * 10**your_balance_info['decimals']) transaction_data = await jupiter.swap( input_mint=move['token_in'], output_mint=move['token_out'], - amount=int(amount_to_swap * 1e6), # Convert to lamports - slippage_bps=50, # Increased to 0.5% + amount=amount, + slippage_bps=100, # Increased to 1% ) - - raw_transaction = VersionedTransaction.from_bytes(base64.b64decode(transaction_data)) signature = private_key.sign_message(message.to_bytes_versioned(raw_transaction.message)) signed_txn = VersionedTransaction.populate(raw_transaction.message, [signature]) @@ -782,6 +795,14 @@ async def follow_move(move): transaction_id = json.loads(result.to_json())['result'] print(f"Follow Transaction Sent: https://solscan.io/tx/{transaction_id}") + notification = ( + f"Move Initiated:\n (decimals: {your_balance_info['decimals']})\n" + f"Swapping {move['percentage_swapped']:.2f}% ({amount_to_swap}) {token_name} ({move['token_in']}) " + f"for {move['token_out']}" + f"\n\nTransaction: {transaction_id}" + ) + logging.info(notification) + await send_telegram_message(notification) tx_details = await get_transaction_details_with_retry(transaction_id)