From 0780878c507cf23a27faa09c1536a8734d9d5118 Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Tue, 5 Nov 2024 16:38:37 +0200 Subject: [PATCH] fixes and starbility ()tetrt --- crypto/sol/.env | 3 + crypto/sol/config.py | 1 + crypto/sol/modules/SolanaAPI.py | 332 +++++++++++++++++--------------- crypto/sol/modules/webui.py | 56 +++++- 4 files changed, 227 insertions(+), 165 deletions(-) diff --git a/crypto/sol/.env b/crypto/sol/.env index df79f65..afb43b4 100644 --- a/crypto/sol/.env +++ b/crypto/sol/.env @@ -15,6 +15,9 @@ TELEGRAM_BOT_TOKEN="6749075936:AAHUHiPTDEIu6JH7S2fQdibwsu6JVG3FNG0" DISPLAY_CURRENCY=USD FOLLOW_AMOUNT=2 +FOLLOW_AMOUNT=percentage + +LIQUIDITY_TOKENS=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v,So11111111111111111111111111111111111111112 # Niki's to Sync: [PROD] FOLLOWED_WALLET="7keSmTZozjmuX66gd9GBSJYEHnMqsyutWpvuuKtXZKDH" diff --git a/crypto/sol/config.py b/crypto/sol/config.py index d8a9dc8..770d50a 100644 --- a/crypto/sol/config.py +++ b/crypto/sol/config.py @@ -21,6 +21,7 @@ SOLANA_HTTP_URL = os.getenv("SOLANA_HTTP_URL") DISPLAY_CURRENCY = os.getenv('DISPLAY_CURRENCY', 'USD') BOT_NAME = os.getenv("BOT_NAME") FOLLOW_AMOUNT = os.getenv('FOLLOW_AMOUNT', 'percentage') +LIQUIDITY_TOKENS = os.getenv('LIQUIDITY_TOKENS', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v').split(',') SOLANA_ENDPOINTS = [ "wss://api.mainnet-beta.solana.com", diff --git a/crypto/sol/modules/SolanaAPI.py b/crypto/sol/modules/SolanaAPI.py index 82c1435..9dc8a76 100644 --- a/crypto/sol/modules/SolanaAPI.py +++ b/crypto/sol/modules/SolanaAPI.py @@ -25,11 +25,18 @@ from solana.rpc.async_api import AsyncClient from solana.rpc.types import TxOpts from solana.rpc.commitment import Confirmed, Finalized, Processed -from solders.transaction import VersionedTransaction + +from solders.rpc.requests import GetTransaction +from solders.signature import Signature +from solders.pubkey import Pubkey from solders.keypair import Keypair -from solana.rpc.async_api import AsyncClient -from solana.rpc.commitment import Processed -from solana.rpc.types import TxOpts +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 solders.instruction import CompiledInstruction +from solders import message from jupiter_python_sdk.jupiter import Jupiter import asyncio @@ -251,8 +258,8 @@ class SolanaAPI: await solana_ws.subscribe() if first_subscription: - await async_safe_call( self.on_initial_subscription, solana_ws.subscription_id) first_subscription = False + await async_safe_call(self.on_initial_subscription, solana_ws.subscription_id) await async_safe_call(self.on_bot_message,f"Solana mainnet connected ({solana_ws.subscription_id})...") @@ -503,7 +510,7 @@ class SolanaAPI: if transfer['mint'] in DEX.TOKENS_INFO or 'decimals' not in DEX.TOKENS_INFO[transfer['mint']]: await self.get_token_metadata_symbol(transfer['mint']) # get actual prices - current_price = await self.get_token_prices([transfer['mint']]) + current_price = await DEX.get_token_prices([transfer['mint']]) if parsed_result["token_in"] is None: parsed_result["token_in"] = transfer['mint'] @@ -529,7 +536,9 @@ class SolanaAPI: parsed_result["percentage_swapped"] = (parsed_result["amount_in"] / parsed_result["before_source_balance"]) * 100 else: # calculate based on total wallet value: FOLLOWED_WALLET_VALUE - parsed_result["percentage_swapped"] = (parsed_result["amount_in_USD"] / DEX.FOLLOWED_WALLET_VALUE) * 100 + # division by 0 + # parsed_result["percentage_swapped"] = (parsed_result["amount_in_USD"] / DEX.FOLLOWED_WALLET_VALUE) * 100 + pass except Exception as e: logging.error(f"Error calculating percentage swapped: {e}") @@ -563,7 +572,7 @@ class SolanaAPI: # return float(balance['uiTokenAmount']['amount']) # return 0.0 - async def get_transaction_details_with_retry(self, transaction_id, retry_delay = 5, max_retries = 16): + async def get_transaction_details_with_retry(self, transaction_id, retry_delay = 5, max_retries = 16, backoff= True): # wait for the transaction to be confirmed # await async_client.wait_for_confirmation(Signature.from_string(transaction_id)) # query every 5 seconds for the transaction details until not None or 30 seconds @@ -574,10 +583,11 @@ class SolanaAPI: break except Exception as e: 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 + if backoff: + retry_delay = retry_delay * 1.2 return tx_details @@ -662,162 +672,174 @@ class SolanaAPI: return 0 async def follow_move(self,move): - your_balances = await DEX.get_wallet_balances(YOUR_WALLET, doGetTokenName=False) - your_balance_info = next((balance for balance in your_balances.values() if balance['address'] == move['token_in']), None) - if your_balance_info is not None: - # Use the balance - print(f"Your balance: {your_balance_info['amount']} {move['symbol_in']}") - else: - print(f"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.") - return - - your_balance = your_balance_info['amount'] + try: + your_balances = await DEX.get_wallet_balances(YOUR_WALLET, doGetTokenName=False) + your_balance_info = next((balance for balance in your_balances.values() if balance['address'] == move['token_in']), None) + if your_balance_info is not None: + # Use the balance + print(f"Your balance: {your_balance_info['amount']} {move['symbol_in']}") + else: + print(f"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.") + return + + your_balance = your_balance_info['amount'] - token_info = DEX.TOKENS_INFO.get(move['token_in']) - token_name_in = token_info.get('symbol') or await SAPI.get_token_metadata_symbol(move['token_in']) - token_name_out = DEX.TOKENS_INFO[move['token_out']].get('symbol') or await SAPI.get_token_metadata_symbol(move['token_out']) + token_info = DEX.TOKENS_INFO.get(move['token_in']) + token_name_in = token_info.get('symbol') or await SAPI.get_token_metadata_symbol(move['token_in']) + token_name_out = DEX.TOKENS_INFO[move['token_out']].get('symbol') or await SAPI.get_token_metadata_symbol(move['token_out']) - if not your_balance: - msg = f"Move not followed:\nNo balance found for token {move['symbol_in']}. Cannot follow move." - logging.warning(msg) - await telegram_utils.send_telegram_message(msg) - return - - if FOLLOW_AMOUNT == 'percentage': - # Calculate the amount to swap based on the same percentage as the followed move - amount_to_swap = your_balance * (move['percentage_swapped'] / 100) - elif FOLLOW_AMOUNT == 'exact': - amount_to_swap = move['amount_in'] - else: - try: - fixed_amount = float(FOLLOW_AMOUNT) # un USD - fixed_amount_in_token = fixed_amount / move["token_in_price"] - amount_to_swap = min(fixed_amount_in_token, your_balance) - except ValueError: - msg = f"Move not followed:\nInvalid FOLLOW_AMOUNT '{FOLLOW_AMOUNT}'. Must be 'percentage' or a number." + if not your_balance: + msg = f"Move not followed:\nNo balance found for token {move['symbol_in']}. Cannot follow move." logging.warning(msg) await telegram_utils.send_telegram_message(msg) return - amount_to_swap = min(amount_to_swap, your_balance) # Ensure we're not trying to swap more than we have - - decimals = token_info.get('decimals') - # Convert to lamports - # if decimals is 6, then amount = amount * 1e6; if 9, then amount = amount * 1e9 - amount = int(amount_to_swap * 10**decimals) - amount = int(amount) - logging.debug(f"Calculated amount in lamports: {amount}") - - if your_balance < amount_to_swap: # should not happen - msg = ( - f"Warning:\n" - f"Insufficient balance: {your_balance:.6f} {token_name_in}. We want to swap {amount_to_swap:.6f}\n({move['symbol_in']}, decimals {token_info.get('decimals')} amount {amount}).\n This will probably fail. But we will try anyway." - ) - logging.warning(msg) - await telegram_utils.send_telegram_message(msg) - try: - - try: - notification = ( - f"Initiating move:\n" - f"Swapping {amount_to_swap:.2f} {token_name_in} for {token_name_out}" - + (f" ({move['percentage_swapped']:.2f}%)" if 'percentage_swapped' in move else "") - ) - # logging.info(notification) - # error_logger.info(notification) - # await telegram_utils.send_telegram_message(notification) - except Exception as e: - logging.error(f"Error sending notification: {e}") - - if self.pk is None: - self.pk = await get_pk() - for retry in range(3): - try: - private_key = Keypair.from_bytes(base58.b58decode(self.pk)) - async_client = AsyncClient(SOLANA_WS_URL) - jupiter = Jupiter(async_client, private_key) - transaction_data = await jupiter.swap( - input_mint=move['token_in'], - output_mint=move['token_out'], - amount=int(amount), - 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}") - raw_transaction = VersionedTransaction.from_bytes(base64.b64decode(transaction_data)) - message = raw_transaction.message - - signature = private_key.sign_message( bytes(message) ) - # signature = private_key.sign_message(message.to_bytes_versioned()) - 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"Follow Transaction Sent: https://solscan.io/tx/{transaction_id}") - # append to notification - notification += f"\n\nTransaction: {transaction_id}" - - await telegram_utils.send_telegram_message(f"Follow Transaction Sent: {transaction_id}") - tx_details = await SAPI.get_transaction_details_with_retry(transaction_id) - - if tx_details is not None: - break + if FOLLOW_AMOUNT == 'percentage': + # Calculate the amount to swap based on the same percentage as the followed move + if move.get('percentage_swapped') is None: + followed_ballances = await DEX.get_wallet_balances(FOLLOWED_WALLET, doGetTokenName=False) + followed_ballance = next((balance for balance in followed_ballances.values() if balance['address'] == move['token_in']), None) + if followed_ballance is not None: + # Use the balance + print(f"Followed balance: {followed_ballance['amount']} {move['symbol_in']}") + move['percentage_swapped'] = (move['amount_in']/followed_ballance['amount']) * 100 + amount_to_swap = your_balance * (move['percentage_swapped'] / 100) else: - logging.warning(f"Failed to get transaction details for {transaction_id}. Probably transaction failed. Retrying again...") - await asyncio.sleep(3) - except Exception as e: - error_message = f"Move Failed:\n{str(e)}\n{transaction_data}\n{move}" - logging.error(error_message) - # log the errors to /logs/errors.log - # error_logger.error(error_message) - # error_logger.exception(e) - await telegram_utils.send_telegram_message(error_message) - amount = int(amount * 0.75) - - await DEX.get_wallet_balances(YOUR_WALLET, doGetTokenName=False) - - try: - if tx_details is None: - logging.info(f"Failed to get transaction details for {transaction_id}") - notification = ( - f"Move Followed, failed to get transaction details.\n" - f"Swapped {amount_to_swap:.6f} {token_name_in} ({move['token_in']}) " - f"(same {move['percentage_swapped']:.2f}% as followed wallet)\n" - f"\n\nTransaction: {transaction_id}" - # log_successful_swap () - ) - + #fallback to 100 USD + amount_to_swap = 100 else: - notification = ( - f"Move Followed:\n" - f"Swapped {amount_to_swap:.6f} {token_name_in} ({move['symbol_in']}) " - f"(same {move['percentage_swapped']:.2f}% as followed wallet)\n" - f"for {tx_details['amount_out']:.2f} {token_name_out}" - # f"Amount In USD: {tr_details['amount_in_USD']}\n" - f"\n\nTransaction: {transaction_id}" - ) - logging.info(notification) - await telegram_utils.send_telegram_message(notification) - except Exception as e: - logging.error(f"Error sending notification: {e}") - - except Exception as e: - error_message = f"Swap Follow Error:\n{str(e)}" - logging.error(error_message) - # log the errors to /logs/errors.log - # error_logger.error(error_message) - # error_logger.exception(e) - # if error_message contains 'Program log: Error: insufficient funds' - if 'insufficient funds' in error_message: - await telegram_utils.send_telegram_message("Insufficient funds. Cannot follow move. Please check your balance.") + amount_to_swap = your_balance * (move['percentage_swapped'] / 100) + elif FOLLOW_AMOUNT == 'exact': + amount_to_swap = move['amount_in'] else: - await telegram_utils.send_telegram_message(error_message) + try: + fixed_amount = float(FOLLOW_AMOUNT) # un USD + fixed_amount_in_token = fixed_amount / move["token_in_price"] + amount_to_swap = min(fixed_amount_in_token, your_balance) + except ValueError: + msg = f"Move not followed:\nInvalid FOLLOW_AMOUNT '{FOLLOW_AMOUNT}'. Must be 'percentage' or a number." + logging.warning(msg) + await telegram_utils.send_telegram_message(msg) + return + amount_to_swap = min(amount_to_swap, your_balance) # Ensure we're not trying to swap more than we have + + decimals = token_info.get('decimals') + # Convert to lamports + # if decimals is 6, then amount = amount * 1e6; if 9, then amount = amount * 1e9 + amount_lamports = int(amount_to_swap * 10**decimals) + logging.debug(f"Calculated amount in lamports: {amount_lamports}") + + if your_balance < amount_to_swap: # should not happen + msg = ( + f"Warning:\n" + f"Insufficient balance: {your_balance:.6f} {token_name_in}. We want to swap {amount_to_swap:.6f}\n({move['symbol_in']}, decimals {token_info.get('decimals')} amount {amount_lamports}).\n This will probably fail. But we will try anyway." + ) + logging.warning(msg) + await telegram_utils.send_telegram_message(msg) + try: + + try: + notification = ( + f"Initiating move:\n" + f"Swapping {amount_to_swap:.2f} {token_name_in} for {token_name_out}" + + (f" ({move['percentage_swapped']:.2f}%)" if 'percentage_swapped' in move else "") + ) + # logging.info(notification) + # error_logger.info(notification) + # await telegram_utils.send_telegram_message(notification) + except Exception as e: + logging.error(f"Error sending notification: {e}") + + if self.pk is None: + self.pk = await get_pk() + for retry in range(3): + try: + private_key = Keypair.from_bytes(base58.b58decode(self.pk)) + async_client = AsyncClient(SOLANA_WS_URL) + jupiter = Jupiter(async_client, private_key) + transaction_data = await jupiter.swap( + input_mint=move['token_in'], + output_mint=move['token_out'], + amount=amount_lamports, + 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}") + raw_transaction = VersionedTransaction.from_bytes(base64.b64decode(transaction_data)) + # message = raw_transaction.message + + # signature = private_key.sign_message( bytes(message) ) + 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'] + notification = f"Follow Transaction Sent:\nTransaction: swapping {amount_to_swap:.2f} {token_name_in}" + logging.info(notification) + await telegram_utils.send_telegram_message(notification) + tx_details = await SAPI.get_transaction_details_with_retry(transaction_id, retry_delay=5, max_retries=6, backoff=False) + + if tx_details is not None: + break + else: + logging.warning(f"Failed to get transaction details for {transaction_id}.\n Probably transaction failed. Retrying again...") + await asyncio.sleep(3) + except Exception as e: + decoded_data = ''# base64.b64decode(transaction_data) + error_message = f"Move Failed:\n{str(e)}\n{decoded_data}\n{move}" + logging.error(error_message) + # log the errors to /logs/errors.log + # error_logger.error(error_message) + # error_logger.exception(e) + await telegram_utils.send_telegram_message(error_message) + amount = int(amount * 0.9) + + await DEX.get_wallet_balances(YOUR_WALLET, doGetTokenName=False) + + try: + if tx_details is None: + logging.info(f"Failed to get transaction details for {transaction_id}") + notification = ( + f"Move Followed, failed to get transaction details.\n" + f"Swapped {amount_to_swap:.6f} {token_name_in} ({move['token_in']}) " + f"(same {move['percentage_swapped']:.2f}% as followed wallet)\n" + f"\n\nTransaction: solscan.io" + # log_successful_swap () + ) + + else: + notification = ( + f"Move Followed:\n" + f"Swapped {amount_to_swap:.6f} {token_name_in} ({move['symbol_in']}) " + f"(same {move['percentage_swapped']:.2f}% as followed wallet)\n" + f"for {tx_details['amount_out']:.2f} {token_name_out}" + # f"Amount In USD: {tr_details['amount_in_USD']}\n" + f"\n\nTransaction: solscan.io" + ) + logging.info(notification) + await telegram_utils.send_telegram_message(notification) + except Exception as e: + logging.error(f"Error sending notification: {e}") + + except Exception as e: + error_message = f"Swap Follow Error:\n{str(e)}" + logging.error(error_message) + # log the errors to /logs/errors.log + # error_logger.error(error_message) + # error_logger.exception(e) + # if error_message contains 'Program log: Error: insufficient funds' + if 'insufficient funds' in error_message: + await telegram_utils.send_telegram_message("Insufficient funds. Cannot follow move. Please check your balance.") + else: + await telegram_utils.send_telegram_message(error_message) + except Exception as e: + logging.error(f"Error following move: {e}") diff --git a/crypto/sol/modules/webui.py b/crypto/sol/modules/webui.py index 142d0c6..06f5310 100644 --- a/crypto/sol/modules/webui.py +++ b/crypto/sol/modules/webui.py @@ -1,8 +1,16 @@ +import asyncio +import os +import sys +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + from flask import Flask, jsonify, request, render_template, redirect, url_for # from flask_oauthlib.client import OAuth from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user import secrets import json +# from crypto.sol.config import LIQUIDITY_TOKENS +from config import LIQUIDITY_TOKENS + from modules import storage, utils, SolanaAPI from modules.utils import async_safe_call import os @@ -98,8 +106,10 @@ def init_app(tr_handler=None): with open( os.path.join(SolanaAPI.root_path, 'logs', f'wh_{current_time}.json') , 'w') as f: json.dump(request_data, f) - process_wh(request_data) - return jsonify({"status": "Webhook processed"}), 200 + # await process_wh(request_data) + # don't wait for the process to finish + asyncio.create_task(process_wh(request_data )) + return jsonify({"status": "OK"}), 200 except Exception as e: logging.error(f"Error processing webhook: {e}") return jsonify({"error": "Failed to process webhook"}), 500 @@ -123,14 +133,40 @@ def init_app(tr_handler=None): if not token_inputs or not token_outputs: logging.warning("Missing token inputs or outputs") return - - tr = { - 'token_in': token_inputs[0]['mint'], - 'token_out': token_outputs[0]['mint'], - 'amount_in': float(token_inputs[0]['rawTokenAmount']['tokenAmount']) / 10**token_inputs[0]['rawTokenAmount']['decimals'], - 'amount_out': float(token_outputs[0]['rawTokenAmount']['tokenAmount']) / 10**token_outputs[0]['rawTokenAmount']['decimals'], - } + usdcMint = LIQUIDITY_TOKENS[0] + tr = {} + if not token_inputs or len(token_inputs) == 0: + logging.info("Assumed USDC as first token. BUY transaction detected") + tr["token_in"] = usdcMint + tr["type"] = "BUY" + tr["amount_in"] = await SolanaAPI.DEX.get_token_prices( + token_outputs[0]["mint"], + int(token_outputs[0]["rawTokenAmount"]["tokenAmount"]) + ) + else: + token_in = token_inputs[0] + tr["token_in"] = token_in["mint"] + tr["token_in_decimals"] = token_in["rawTokenAmount"]["decimals"] + tr["amount_in"] = float(int(token_in["rawTokenAmount"]["tokenAmount"]) / 10**token_in["rawTokenAmount"]["decimals"]) + # 'amount_in': float(token_inputs[0]['rawTokenAmount']['tokenAmount']) / 10**token_inputs[0]['rawTokenAmount']['decimals'], + + if not token_outputs or len(token_outputs) == 0: + logging.info("Assumed USDC as second token. SELL transaction detected") + tr["token_out"] = usdcMint + tr["type"] = "SELL" + tr["amount_out"] = await SolanaAPI.DEX.get_token_prices( + token_inputs[0]["mint"], + int(token_inputs[0]["rawTokenAmount"]["tokenAmount"]) + ) + else: + token_out = token_outputs[0] + tr["token_out"] = token_out["mint"] + tr["token_out_decimals"] = token_out["rawTokenAmount"]["decimals"] + tr["amount_out"] = float(int(token_out["rawTokenAmount"]["tokenAmount"]) / 10**token_out["rawTokenAmount"]["decimals"]) + #'amount_out': float(token_outputs[0]['rawTokenAmount']['tokenAmount']) / 10**token_outputs[0]['rawTokenAmount']['decimals'], + + if not tr["token_in"] or not tr["token_out"] or tr["amount_in"] == 0 or tr["amount_out"] == 0: logging.warning("Incomplete swap details found in logs. Getting details from transaction") tx_signature = data[0].get('signature') @@ -150,7 +186,7 @@ def init_app(tr_handler=None): tr['value_out_USD'] = prices.get(tr['token_out'], 0) * tr['amount_out'] notification = ( - f"Got WH notification:: {tr['amount_in']} {tr['symbol_in']} swapped for {tr['symbol_out']} \n" + f"Got WH notification:: {tr['amount_in']} {tr['symbol_in']} swapped for {tr['amount_out']} {tr['symbol_out']} ${tr['value_out_USD']}\n" ) logging.info(notification) await utils.telegram_utils.send_telegram_message(notification)