swap works again reliably

This commit is contained in:
Dobromir Popov 2024-11-12 22:11:29 +02:00
parent 291a5fed2c
commit eebba5d6b4
3 changed files with 142 additions and 96 deletions

View File

@ -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

View File

@ -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(
# 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:\n<b>Transaction:</b> <a href='https://solscan.io/tx/{transaction_id}'>swapping {amount_to_swap:.2f} {token_name_in}</a>"
notification = f"Follow {move.get('type', 'SWAP').upper()} Success:\n<b>Transaction:</b> <a href='https://solscan.io/tx/{transaction_id}'> swapping {amount_to_swap:.2f} {token_name_in}</a>"
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):

View File

@ -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/<int:wallet_id>/transactions', methods=['GET'])
@login_required
@login_required
def get_transactions(wallet_id):
transactions = storage.get_transactions(wallet_id)
return jsonify(transactions)
@app.route('/wallet/<int:wallet_id>/holdings', methods=['GET'])
@login_required
@login_required
def get_holdings(wallet_id):
holdings = storage.get_holdings(wallet_id)
return jsonify(holdings)