diff --git a/crypto/sol/.env b/crypto/sol/.env index e14028e..872d843 100644 --- a/crypto/sol/.env +++ b/crypto/sol/.env @@ -22,8 +22,8 @@ DEVELOPER_CHAT_ID="777826553" TELEGRAM_BOT_TOKEN="6749075936:AAHUHiPTDEIu6JH7S2fQdibwsu6JVG3FNG0" DISPLAY_CURRENCY=USD -FOLLOW_AMOUNT=3 -#FOLLOW_AMOUNT=percentage +#FOLLOW_AMOUNT=3 +FOLLOW_AMOUNT=percentage LIQUIDITY_TOKENS=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v,So11111111111111111111111111111111111111112 diff --git a/crypto/sol/modules/SolanaAPI.py b/crypto/sol/modules/SolanaAPI.py index 97e373a..59e57f0 100644 --- a/crypto/sol/modules/SolanaAPI.py +++ b/crypto/sol/modules/SolanaAPI.py @@ -1,3 +1,4 @@ +import base64 import struct import sys import os @@ -50,7 +51,7 @@ from datetime import datetime from solana.rpc.types import TokenAccountOpts, TxOpts from typing import List, Dict, Any, Tuple import traceback -import base64 +import httpx # # # solders/solana libs (solana_client) # # # from spl.token._layouts import MINT_LAYOUT @@ -234,6 +235,17 @@ class SolanaWS: class SolanaAPI: pk = None + + ENDPOINT_APIS_URL = { + "QUOTE": "https://quote-api.jup.ag/v6/quote?", + "SWAP": "https://quote-api.jup.ag/v6/swap", + "OPEN_ORDER": "https://jup.ag/api/limit/v1/createOrder", + "CANCEL_ORDERS": "https://jup.ag/api/limit/v1/cancelOrders", + "QUERY_OPEN_ORDERS": "https://jup.ag/api/limit/v1/openOrders?wallet=", + "QUERY_ORDER_HISTORY": "https://jup.ag/api/limit/v1/orderHistory", + "QUERY_TRADE_HISTORY": "https://jup.ag/api/limit/v1/tradeHistory" + } + def __init__(self, process_transaction_callback = None, on_initial_subscription_callback = None, on_bot_message=None): self.process_transaction = process_transaction_callback self.on_initial_subscription = on_initial_subscription_callback @@ -247,6 +259,7 @@ class SolanaAPI: self.dex = DEX self.solana_ws = SolanaWS(on_message=self.process_transaction) + async def process_messages(self, solana_ws): while True: message = await solana_ws.message_queue.get() @@ -587,7 +600,7 @@ class SolanaAPI: 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}. retry in {retry_delay} s.") + logging.info(f"({_} of {max_retries}) Waiting for transaction details for {transaction_id} [ retry in {retry_delay} s.]") await asyncio.sleep(retry_delay) if backoff: retry_delay = retry_delay * 1.2 @@ -672,6 +685,100 @@ class SolanaAPI: logging.error(f"Error getting balance for {token_address} in {wallet_address}: {str(e)} \r\n {e}") return 0 + + + async def swap_on_jupiter( + self, + input_mint: str, + output_mint: str, + amount: int, + slippage_bps: int = 1, + swap_mode: str = "ExactIn", + priority_fee: int = 0, + only_direct_routes: bool = False, + as_legacy_transaction: bool = False, + exclude_dexes: list = None, + max_accounts: int = None + ) -> str: + """Perform a swap on Jupiter with an option to set priority. + + Args: + input_mint (str): Input token mint. + output_mint (str): Output token mint. + amount (int): Amount to swap, considering token decimals. + slippage_bps (int): Slippage in basis points. + swap_mode (str): Swap mode, either 'ExactIn' or 'ExactOut'. + priority_level (int): Priority level for the transaction fee. + only_direct_routes (bool): Limit to direct routes only. + as_legacy_transaction (bool): Use legacy transaction format. + exclude_dexes (list): List of DEXes to exclude. + max_accounts (int): Max number of accounts involved. + + Returns: + str: Serialized transaction data for the swap. + """ + # Get a quote from Jupiter + quote_url = self.ENDPOINT_APIS_URL['QUOTE'] + "inputMint=" + input_mint + "&outputMint=" + output_mint + "&amount=" + str(amount) + "&swapMode=" + swap_mode + "&onlyDirectRoutes=" + str(only_direct_routes).lower() + "&asLegacyTransaction=" + str(as_legacy_transaction).lower() + if slippage_bps: + quote_url += "&slippageBps=" + str(slippage_bps) + if exclude_dexes: + quote_url += "&excludeDexes=" + ','.join(exclude_dexes).lower() + if max_accounts: + quote_url += "&maxAccounts=" + str(max_accounts) + + quote_response = httpx.get(url=quote_url).json() + + try: + quote_response['routePlan'] + except: + raise Exception(quote_response['error']) + + + # Prepare transaction parameters + fees = "auto" if priority_fee == 0 else priority_fee + + pair = Keypair.from_bytes(base58.b58decode(self.pk)) + pubK = pair.pubkey().__str__() + transaction_parameters = { + "quoteResponse":quote_response, + "userPublicKey": pubK, + "wrapAndUnwrapSol": True, + "computeUnitPriceMicroLamports":fees + } + # This will raise an error if data isn't JSON serializable + json_string = json.dumps(transaction_parameters) + validated_data = json.loads(json_string) + + response = httpx.post(url=self.ENDPOINT_APIS_URL['SWAP'], json=validated_data) + response_data = response.json() + + # headers = { + # 'Content-Type': 'application/json', + # 'Accept': 'application/json' + # } + # response = requests.request("POST", self.ENDPOINT_APIS_URL['SWAP'], headers=headers, data=validated_data) + # response_data = response.json() + + # result = response_data['swapTransaction'] + + # # # Send the swap request to Jupiter + # async with httpx.AsyncClient() as client: + # response = await client.post( + # self.ENDPOINT_APIS_URL['SWAP'], + # json=validated_data + # ) + # response_data = response.json() + + result = response_data['swapTransaction'] + try: + response_data['swapTransaction'] + return response_data['swapTransaction'] + except: + raise Exception(response_data['error']) + # Return the serialized transaction + return result + + async def follow_move(self,move): try: your_balances = await DEX.get_wallet_balances(YOUR_WALLET, doGetTokenName=False) @@ -756,101 +863,27 @@ class SolanaAPI: if self.pk is None: self.pk = await get_pk() - for retry in range(1): + for retry in range(2): 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( + jupiter = Jupiter(async_client, private_key) + + # https://station.jup.ag/api-v6/post-swap + #transaction_data = await jupiter.swap( + transaction_data = await self.swap_on_jupiter( input_mint=move['token_in'], output_mint=move['token_out'], amount=amount_lamports, slippage_bps=300, # Increased to 3% + #priority_fee= 100_000 commented for auto ) logging.info(f"Initiating move. Transaction data:\n {transaction_data}") raw_transaction = VersionedTransaction.from_bytes(base64.b64decode(transaction_data)) - fee = await async_client.get_fee_for_message(raw_transaction.message) - priority_fee = fee.value or 100_000 - - - # fee = await async_client.get_fee_for_message(transaction_data) - # priority_fee = 0 - # if PRIORITY: - # priority_fee = 100 * PRIORITY # defalt if we can't get current rate - # try: - # priority_fee = await calculate_priority_fee(async_client, PRIORITY) - # except: - # logging.warning(f"Failed to get priority fee. Using default value: {priority_fee}") - - # error_logger.info(f"Initiating move. Transaction data:\n {transaction_data}") - # message = raw_transaction.message - - # Add compute budget instruction to set priority fee - # from solders.compute_budget import set_compute_unit_price - # compute_budget_instruction = set_compute_unit_price(priority_fee) - - # Add compute budget instruction to the transaction - # raw_transaction.message.add_instruction(compute_budget_instruction) - - # Create new instructions list with compute budget instruction first - # new_instructions = [compute_budget_instruction] + list(raw_transaction.message.instructions) - - # # Create a new message with the updated instructions - # from solders.message import MessageV0 - # new_message = MessageV0( - # instructions=new_instructions, - # address_table_lookups=raw_transaction.message.address_table_lookups, - # recent_blockhash=raw_transaction.message.recent_blockhash, - # payer=raw_transaction.message.payer - # ) - # working - no priority fee signature = private_key.sign_message(message.to_bytes_versioned(raw_transaction.message)) signed_txn = VersionedTransaction.populate(raw_transaction.message, [signature]) - # # # # # # # # # # # # # # # # # # - # new - not working - # signature = private_key.sign_message(new_message.to_bytes_versioned()) - # signed_txn = VersionedTransaction.populate(new_message, [signature]) - # from solders.compute_budget import set_compute_unit_price, ID as COMPUTE_BUDGET_ID - - # priority_fee_ix = set_compute_unit_price(priority_fee) - - # # Get the current message - # msg = raw_transaction.message - - # new_account_keys = msg.account_keys - # program_id_index = 0 - # if COMPUTE_BUDGET_ID not in msg.account_keys: - # new_account_keys = msg.account_keys + [COMPUTE_BUDGET_ID] - # program_id_index = len(msg.account_keys) # Index of the newly added program ID - # else: - # new_account_keys = msg.account_keys - # program_id_index = msg.account_keys.index(COMPUTE_BUDGET_ID) - - # # Compile the priority fee instruction - # compiled_priority_fee_ix = CompiledInstruction( - # program_id_index=program_id_index, - # accounts=bytes([]), - # data=priority_fee_ix.data - # ) - - # # Add priority fee instruction at the beginning - # new_instructions = [compiled_priority_fee_ix] + msg.instructions - - # # Create new message with updated instructions - # new_message = Message.new_with_compiled_instructions( - # num_required_signatures=msg.header.num_required_signatures, - # num_readonly_signed_accounts=msg.header.num_readonly_signed_accounts, - # num_readonly_unsigned_accounts=msg.header.num_readonly_unsigned_accounts, - # account_keys=new_account_keys, - # recent_blockhash=msg.recent_blockhash, - # instructions=new_instructions - # ) - - # signature = private_key.sign_message(message.to_bytes_versioned(new_message)) - # signed_txn = VersionedTransaction.populate(new_message, [signature]) - # # # # # # # # # # # # # # # # # # opts = TxOpts( skip_preflight=False, @@ -861,7 +894,7 @@ class SolanaAPI: 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}" + notification = f"Follow {move.get('type', 'SWAP').upper()} Success:\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=10, backoff=False) @@ -879,7 +912,7 @@ class SolanaAPI: # error_logger.error(error_message) # error_logger.exception(e) await telegram_utils.send_telegram_message(error_message) - amount = int(amount * 0.9) + amount_to_swap = int(amount_to_swap * 0.9) await DEX.get_wallet_balances(YOUR_WALLET, doGetTokenName=False) @@ -922,19 +955,20 @@ class SolanaAPI: except Exception as e: logging.error(f"Error following move: {e}") - async def calculate_priority_fee(async_client, priority_level=5): +async def calculate_priority_fee(self, async_client, priority_level=5): + try: recent_fees = await async_client.get_recent_prioritization_fees() - if not recent_fees: - return 1000 # fallback value in microlamports + if not recent_fees or len(recent_fees) == 0: + return 100_000 # fallback value in microlamports # Calculate average and max fees - fees = [fee.prioritization_fee for fee in recent_fees] + fees = [slot_fee.prioritization_fee for slot_fee in recent_fees] avg_fee = sum(fees) / len(fees) max_fee = max(fees) # Calculate base fee (weighted average between mean and max) - base_fee = (2 * avg_fee + max_fee) / 3 # You can adjust this weighting + base_fee = (2 * avg_fee + max_fee) / 3 # Calculate scaling factor (priority_level / 5) # priority 5 = 1x base_fee @@ -945,8 +979,11 @@ class SolanaAPI: final_fee = int(base_fee * scaling_factor) # Set minimum fee to avoid too low values - return max(final_fee, 100) # minimum 100 microlamports - + return max(final_fee, 100_001) # minimum 100,000 microlamports + + except Exception as e: + logging.warning(f"Error calculating priority fee: {str(e)}") + return 100_000 # fallback value in microlamports class SolanaDEX: def __init__(self, DISPLAY_CURRENCY: str): @@ -1269,4 +1306,4 @@ class SolanaDEX: DEX = SolanaDEX(DISPLAY_CURRENCY) -SAPI = SolanaAPI( on_initial_subscription_callback=DEX.list_initial_wallet_states(FOLLOWED_WALLET,YOUR_WALLET)) \ No newline at end of file +SAPI = SolanaAPI( on_initial_subscription_callback=DEX.list_initial_wallet_states(FOLLOWED_WALLET,YOUR_WALLET)) diff --git a/crypto/sol/modules/webui.py b/crypto/sol/modules/webui.py index 6dce7e2..01b1c05 100644 --- a/crypto/sol/modules/webui.py +++ b/crypto/sol/modules/webui.py @@ -10,7 +10,7 @@ from flask_login import LoginManager, UserMixin, login_user, login_required, log import secrets import json # from crypto.sol.config import LIQUIDITY_TOKENS -from config import LIQUIDITY_TOKENS +from config import LIQUIDITY_TOKENS, YOUR_WALLET from modules import storage, utils, SolanaAPI from modules.utils import async_safe_call, decode_instruction_data @@ -24,6 +24,8 @@ def init_app(tr_handler=None): global on_transaction on_transaction = tr_handler app = Flask(__name__, template_folder='../templates', static_folder='../static') + + app.secret_key = secrets.token_hex(16) executor = ThreadPoolExecutor(max_workers=10) # Adjust the number of workers as needed login_manager = LoginManager(app) login_manager.login_view = 'login' @@ -423,6 +425,11 @@ def init_app(tr_handler=None): def index(): return render_template('index.html') + @login_manager.unauthorized_handler + def unauthorized(): + return redirect('/login?next=' + request.path) + # return jsonify({'error': 'Unauthorized'}), 401 + @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': @@ -460,12 +467,14 @@ def init_app(tr_handler=None): @app.route('/wallet//transactions', methods=['GET']) @login_required + @login_required def get_transactions(wallet_id): transactions = storage.get_transactions(wallet_id) return jsonify(transactions) @app.route('/wallet//holdings', methods=['GET']) @login_required + @login_required def get_holdings(wallet_id): holdings = storage.get_holdings(wallet_id) return jsonify(holdings)