Merge commit 'aab85b4b099f098f459fe9fc350c591f069d3f79'

This commit is contained in:
Dobromir Popov
2024-10-21 14:53:17 +03:00
6 changed files with 182 additions and 2847 deletions

View File

@ -1,4 +1,6 @@
import asyncio
import uvicorn
from asgiref.wsgi import WsgiToAsgi
import websockets
import json
from flask import Flask, render_template, request, jsonify
@ -34,10 +36,10 @@ from dotenv import load_dotenv,set_key
import aiohttp
from typing import List, Dict
import requests
import threading
import re
from typing import List, Dict, Any, Tuple
import random
from threading import Thread
from modules.webui import init_app
@ -133,6 +135,7 @@ def get_latest_log_file():
return None
# Flask route to retry processing the last log
@app.route('/retry', methods=['GET'])
@app.route('/retry-last-log', methods=['GET'])
async def retry_last_log():
latest_log_file = get_latest_log_file()
@ -157,9 +160,50 @@ async def retry_last_log():
return jsonify({"error": "Failed to process log"}), 500
#const webhookPath = `/tr/${followedWallet.toBase58()}/${logs.signature}`;
@app.route('/tr/<wallet>/<tx_signature>', methods=['GET', 'POST'])
async def transaction_notified(wallet, tx_signature):
try:
logger.info(f"Processing transaction notification for wallet: {wallet}, tx: {tx_signature}")
# Process the transaction
# tr = await get_swap_transaction_details(tx_signature)
tr = await get_transaction_details_info(tx_signature, [])
get_token_metadata_symbol(tr)
# ToDo - probably optimize
await follow_move(tr['token_in'])
await follow_move(tr['token_out'])
await save_token_info()
return jsonify(tr), 200
except Exception as e:
logging.error(f"Error processing transaction: {e}")
return jsonify({"error": "Failed to process transaction"}), 500
# Create the bot with the custom connection pool
bot = None
# 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_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)
# Token addresses (initialize with some known tokens)
TOKEN_ADDRESSES = {
"SOL": "So11111111111111111111111111111111111111112",
"USDC": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"TARD": "4nfn86ssbv7wiqcsw7bpvn46k24jhe334fudtyxhp1og",
}
TOKENS_INFO = {}
try:
@ -359,6 +403,80 @@ async def get_token_metadata(mint_address):
return None
async def get_wallet_balances(wallet_address, doGetTokenName=True):
balances = {}
logging.info(f"Getting balances for wallet: {wallet_address}")
global TOKENS_INFO
try:
response = await solana_client.get_token_accounts_by_owner_json_parsed(
Pubkey.from_string(wallet_address),
opts=TokenAccountOpts(
program_id=Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
),
commitment=Confirmed
)
if response.value:
for account in response.value:
try:
parsed_data = account.account.data.parsed
if isinstance(parsed_data, dict) and 'info' in parsed_data:
info = parsed_data['info']
if isinstance(info, dict) and 'mint' in info and 'tokenAmount' in info:
mint = info['mint']
decimals = info['tokenAmount']['decimals']
amount = float(info['tokenAmount']['amount'])/10**decimals
if amount > 0:
if mint in TOKENS_INFO:
token_name = TOKENS_INFO[mint].get('symbol')
elif doGetTokenName:
token_name = await get_token_metadata_symbol(mint) or 'N/A'
# sleep for 1 second to avoid rate limiting
await asyncio.sleep(2)
TOKENS_INFO[mint]['holdedAmount'] = round(amount, decimals)
TOKENS_INFO[mint]['decimals'] = decimals
balances[mint] = {
'name': token_name or 'N/A',
'address': mint,
'amount': amount,
'decimals': decimals
}
# sleep for 1 second to avoid rate limiting
logging.debug(f"Account balance for {token_name} ({mint}): {amount}")
else:
logging.warning(f"Unexpected data format for account: {account}")
except Exception as e:
logging.error(f"Error parsing account data: {str(e)}")
sol_balance = await solana_client.get_balance(Pubkey.from_string(wallet_address))
if sol_balance.value is not None:
balances['SOL'] = {
'name': 'SOL',
'address': 'SOL',
'amount': sol_balance.value / 1e9
}
else:
logging.warning(f"SOL balance response missing for wallet: {wallet_address}")
except Exception as e:
logging.error(f"Error getting wallet balances: {str(e)}")
logging.info(f"Found {len(response.value)} ({len(balances)} non zero) token accounts for wallet: {wallet_address}")
return balances
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]
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_swap_transaction_details(tx_signature_str):
@ -428,8 +546,9 @@ async def get_transaction_details_with_retry(transaction_id, retry_delay = 5, ma
if tx_details is not None:
break
except Exception as e:
logging.error(f"Error fetching transaction details for '{transaction_id}': {e}")
logging.info(f"({_} of {max_retries}) Waiting for transaction details for {transaction_id}")
logging.error(f"Error fetching transaction details: {e}")
retry_delay = retry_delay * 1.2
logging.info(f"({_} of {max_retries}) Waiting for transaction details for {transaction_id}. retry in {retry_delay} s.")
await asyncio.sleep(retry_delay)
retry_delay *= 1.2
return tx_details
@ -478,7 +597,7 @@ async def process_log(log_result):
before_source_balance = 0
source_token_change = 0
i = 0
while i < len(logs):
log_entry = logs[i]
@ -501,7 +620,7 @@ async def process_log(log_result):
i += 1
# calculatte percentage swapped by digging before_source_balance, source_token_change and after_source_balance
# calculate 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",
@ -568,6 +687,7 @@ async def process_log(log_result):
PROCESSING_LOG = False
return tr_details
# "Program log: Instruction: Swap2",
# "Program log: order_id: 13985890735038016",
# "Program log: AbrMJWfDVRZ2EWCQ1xSCpoVeVgZNpq1U2AoYG98oRXfn", source
@ -615,8 +735,8 @@ async def follow_move(move):
# Use the balance
print(f"Your balance: {your_balance_info['amount']} {move['symbol_in']}")
else:
print("No ballance found for {move['symbol_in']}. Skipping move.")
await telegram_utils.send_telegram_message(f"No ballance found for {move['symbol_in']}. Skipping move.")
print(f"No ballance found for {move['symbol_in']}. Skipping move.")
await send_telegram_message(f"No ballance found for {move['symbol_in']}. Skipping move.")
return
your_balance = your_balance_info['amount']
@ -687,7 +807,7 @@ async def follow_move(move):
input_mint=move['token_in'],
output_mint=move['token_out'],
amount=amount,
slippage_bps=100, # Increased to 1%
slippage_bps=300, # Increased to 3%
)
logging.info(f"Initiating move. Transaction data:\n {transaction_data}")
error_logger.info(f"Initiating move. Transaction data:\n {transaction_data}")
@ -769,7 +889,7 @@ SOLANA_ENDPOINTS = [
# "wss://mainnet.rpcpool.com",
]
PING_INTERVAL = 30
SUBSCRIBE_INTERVAL = 1*60 # Resubscribe every 10 minutes
SUBSCRIBE_INTERVAL = 10*60 # Resubscribe every 10 minutes
# async def heartbeat(websocket):
@ -802,11 +922,11 @@ async def wallet_watch_loop():
subscription_id = await subscribe(websocket)
if subscription_id is not None:
await send_telegram_message(f"Solana mainnet connected ({subscription_id})...")
# await send_telegram_message(f"Solana mainnet connected ({subscription_id})...")
if _first_subscription:
asyncio.create_task( list_initial_wallet_states())
_first_subscription = False
_process_task = asyncio.create_task(process_messages(websocket, subscription_id))
_process_task = asyncio.create_task(process_messages(websocket))
while True:
try:# drop subscription now
await process_messages(websocket, subscription_id)
@ -830,7 +950,7 @@ async def wallet_watch_loop():
# Already subscribed
logger.info("Already subscribed, continuing with existing subscription")
if subscription_id:
process_task = asyncio.create_task(process_messages(websocket, subscription_id))
process_task = asyncio.create_task(process_messages(websocket))
else:
# process_messages completed (shouldn't happen unless there's an error)
@ -848,7 +968,7 @@ async def wallet_watch_loop():
await unsubscribe(websocket, subscription_id)
await send_telegram_message("reconnecting...")
logger.info(f"Attempting to reconnect in {reconnect_delay} seconds...")
websocket.close()
await websocket.close()
except Exception as e:
logger.error(f"An unexpected error occurred - breaking watch loop: {e}")
@ -868,22 +988,7 @@ async def subscribe(websocket):
try:
await websocket.send(json.dumps(request))
logger.info("Subscription request sent")
response = await websocket.recv()
response_data = json.loads(response)
if 'result' in response_data:
subscription_id = response_data['result']
logger.info(f"Subscription successful. Subscription id: {subscription_id}")
return subscription_id
else:
logger.warning(f"Unexpected response: {response_data}")
return None
except websockets.exceptions.ConnectionClosedError as e:
logger.error(f"Connection closed unexpectedly: {e}")
await send_telegram_message("Connection to Solana network was closed. Not listening for transactions right now. Attempting to reconnect...")
await websocket.close()
return None
return await process_messages(websocket)
except Exception as e:
logger.error(f"An unexpected error occurred: {e}")
return None
@ -900,14 +1005,23 @@ async def unsubscribe(websocket, subscription_id):
logger.info(f"Unsubscribed from subscription id: {subscription_id}")
subscription_id = None
async def process_messages(websocket, subscription_id):
async def process_messages(websocket):
try:
while True:
response = await websocket.recv()
response_data = json.loads(response)
logger.debug(f"Received response: {response_data}")
if 'params' in response_data:
if 'result' in response_data:
new_sub_id = response_data['result']
if int(new_sub_id) > 1:
subscription_id = new_sub_id
logger.info(f"Subscription successful. New id: {subscription_id}")
elif new_sub_id:
logger.info(f"Existing subscription confirmed: {subscription_id}")
else: return None
return subscription_id
elif 'params' in response_data:
log = response_data['params']['result']
logger.debug(f"Received transaction log: {log}")
asyncio.create_task(process_log(log))
@ -916,7 +1030,7 @@ async def process_messages(websocket, subscription_id):
except websockets.exceptions.ConnectionClosedError as e:
logger.error(f"Connection closed unexpectedly: {e}")
await send_telegram_message("Connection to Solana network was closed. Not listening for transactions right now. Attempting to reconnect...")
# await send_telegram_message("Connection to Solana network was closed. Not listening for transactions right now. Attempting to reconnect...")
pass
except json.JSONDecodeError as e:
logger.error(f"Failed to decode JSON: {e}")
@ -946,9 +1060,8 @@ async def check_PK():
await telegram_utils.send_telegram_message("<b>Warning:</b> Private key not found in environment variables. Will not be able to sign transactions.")
solanaAPI = SolanaAPI(process_transaction_callback=process_log)
# Convert Flask app to ASGI
asgi_app = WsgiToAsgi(app)
async def main():
global solanaAPI, bot, PROCESSING_LOG
@ -985,19 +1098,35 @@ async def main():
async def run_flask():
# loop = asyncio.get_running_loop()
# await loop.run_in_executor(None, lambda: app.run(debug=False, port=3001, use_reloader=False))
app = init_app()
loop = asyncio.get_running_loop()
await loop.run_in_executor(None, lambda: app.run(debug=False, port=3001, use_reloader=False))
def run_asyncio_loop(loop):
asyncio.set_event_loop(loop)
loop.run_forever()
async def run_all():
await asyncio.gather(
init_db(),
main(),
run_flask()
)
main_task = asyncio.create_task(main())
await main_task
if __name__ == '__main__':
asyncio.run(run_all())
# Create a new event loop
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
# Start the asyncio loop in a separate thread
thread = Thread(target=run_asyncio_loop, args=(loop,))
thread.start()
# Schedule the run_all coroutine in the event loop
asyncio.run_coroutine_threadsafe(run_all(), loop)
# Run Uvicorn in the main thread
uvicorn.run(
"app:asgi_app", # Replace 'app' with the actual name of this Python file if different
host="127.0.0.1",
port=3001,
log_level="debug",
reload=True
)
# When Uvicorn exits, stop the asyncio loop
loop.call_soon_threadsafe(loop.stop)
thread.join()