Merge commit 'aab85b4b099f098f459fe9fc350c591f069d3f79'
This commit is contained in:
@ -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()
|
Reference in New Issue
Block a user