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" TELEGRAM_BOT_TOKEN="6749075936:AAHUHiPTDEIu6JH7S2fQdibwsu6JVG3FNG0"
DISPLAY_CURRENCY=USD DISPLAY_CURRENCY=USD
FOLLOW_AMOUNT=3 #FOLLOW_AMOUNT=3
#FOLLOW_AMOUNT=percentage FOLLOW_AMOUNT=percentage
LIQUIDITY_TOKENS=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v,So11111111111111111111111111111111111111112 LIQUIDITY_TOKENS=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v,So11111111111111111111111111111111111111112

View File

@ -1,3 +1,4 @@
import base64
import struct import struct
import sys import sys
import os import os
@ -50,7 +51,7 @@ from datetime import datetime
from solana.rpc.types import TokenAccountOpts, TxOpts from solana.rpc.types import TokenAccountOpts, TxOpts
from typing import List, Dict, Any, Tuple from typing import List, Dict, Any, Tuple
import traceback import traceback
import base64 import httpx
# # # solders/solana libs (solana_client) # # # # # # solders/solana libs (solana_client) # # #
from spl.token._layouts import MINT_LAYOUT from spl.token._layouts import MINT_LAYOUT
@ -234,6 +235,17 @@ class SolanaWS:
class SolanaAPI: class SolanaAPI:
pk = None 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): def __init__(self, process_transaction_callback = None, on_initial_subscription_callback = None, on_bot_message=None):
self.process_transaction = process_transaction_callback self.process_transaction = process_transaction_callback
self.on_initial_subscription = on_initial_subscription_callback self.on_initial_subscription = on_initial_subscription_callback
@ -247,6 +259,7 @@ class SolanaAPI:
self.dex = DEX self.dex = DEX
self.solana_ws = SolanaWS(on_message=self.process_transaction) self.solana_ws = SolanaWS(on_message=self.process_transaction)
async def process_messages(self, solana_ws): async def process_messages(self, solana_ws):
while True: while True:
message = await solana_ws.message_queue.get() message = await solana_ws.message_queue.get()
@ -587,7 +600,7 @@ class SolanaAPI:
except Exception as e: except Exception as e:
logging.error(f"Error fetching transaction details: {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) await asyncio.sleep(retry_delay)
if backoff: if backoff:
retry_delay = retry_delay * 1.2 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}") logging.error(f"Error getting balance for {token_address} in {wallet_address}: {str(e)} \r\n {e}")
return 0 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): async def follow_move(self,move):
try: try:
your_balances = await DEX.get_wallet_balances(YOUR_WALLET, doGetTokenName=False) your_balances = await DEX.get_wallet_balances(YOUR_WALLET, doGetTokenName=False)
@ -756,101 +863,27 @@ class SolanaAPI:
if self.pk is None: if self.pk is None:
self.pk = await get_pk() self.pk = await get_pk()
for retry in range(1): for retry in range(2):
try: try:
private_key = Keypair.from_bytes(base58.b58decode(self.pk)) private_key = Keypair.from_bytes(base58.b58decode(self.pk))
async_client = AsyncClient(SOLANA_WS_URL) async_client = AsyncClient(SOLANA_WS_URL)
jupiter = Jupiter(async_client, private_key) 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'], input_mint=move['token_in'],
output_mint=move['token_out'], output_mint=move['token_out'],
amount=amount_lamports, amount=amount_lamports,
slippage_bps=300, # Increased to 3% slippage_bps=300, # Increased to 3%
#priority_fee= 100_000 commented for auto
) )
logging.info(f"Initiating move. Transaction data:\n {transaction_data}") logging.info(f"Initiating move. Transaction data:\n {transaction_data}")
raw_transaction = VersionedTransaction.from_bytes(base64.b64decode(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 # working - no priority fee
signature = private_key.sign_message(message.to_bytes_versioned(raw_transaction.message)) signature = private_key.sign_message(message.to_bytes_versioned(raw_transaction.message))
signed_txn = VersionedTransaction.populate(raw_transaction.message, [signature]) 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( opts = TxOpts(
skip_preflight=False, skip_preflight=False,
@ -861,7 +894,7 @@ class SolanaAPI:
result = await async_client.send_raw_transaction(txn=bytes(signed_txn), opts=opts) result = await async_client.send_raw_transaction(txn=bytes(signed_txn), opts=opts)
transaction_id = json.loads(result.to_json())['result'] 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) logging.info(notification)
await telegram_utils.send_telegram_message(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) 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.error(error_message)
# error_logger.exception(e) # error_logger.exception(e)
await telegram_utils.send_telegram_message(error_message) 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) await DEX.get_wallet_balances(YOUR_WALLET, doGetTokenName=False)
@ -922,19 +955,20 @@ class SolanaAPI:
except Exception as e: except Exception as e:
logging.error(f"Error following move: {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() recent_fees = await async_client.get_recent_prioritization_fees()
if not recent_fees: if not recent_fees or len(recent_fees) == 0:
return 1000 # fallback value in microlamports return 100_000 # fallback value in microlamports
# Calculate average and max fees # 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) avg_fee = sum(fees) / len(fees)
max_fee = max(fees) max_fee = max(fees)
# Calculate base fee (weighted average between mean and max) # 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) # Calculate scaling factor (priority_level / 5)
# priority 5 = 1x base_fee # priority 5 = 1x base_fee
@ -945,8 +979,11 @@ class SolanaAPI:
final_fee = int(base_fee * scaling_factor) final_fee = int(base_fee * scaling_factor)
# Set minimum fee to avoid too low values # 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: class SolanaDEX:
def __init__(self, DISPLAY_CURRENCY: str): def __init__(self, DISPLAY_CURRENCY: str):
@ -1269,4 +1306,4 @@ class SolanaDEX:
DEX = SolanaDEX(DISPLAY_CURRENCY) DEX = SolanaDEX(DISPLAY_CURRENCY)
SAPI = SolanaAPI( on_initial_subscription_callback=DEX.list_initial_wallet_states(FOLLOWED_WALLET,YOUR_WALLET)) SAPI = SolanaAPI( on_initial_subscription_callback=DEX.list_initial_wallet_states(FOLLOWED_WALLET,YOUR_WALLET))

View File

@ -10,7 +10,7 @@ from flask_login import LoginManager, UserMixin, login_user, login_required, log
import secrets import secrets
import json import json
# from crypto.sol.config import LIQUIDITY_TOKENS # 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 import storage, utils, SolanaAPI
from modules.utils import async_safe_call, decode_instruction_data from modules.utils import async_safe_call, decode_instruction_data
@ -24,6 +24,8 @@ def init_app(tr_handler=None):
global on_transaction global on_transaction
on_transaction = tr_handler on_transaction = tr_handler
app = Flask(__name__, template_folder='../templates', static_folder='../static') 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 executor = ThreadPoolExecutor(max_workers=10) # Adjust the number of workers as needed
login_manager = LoginManager(app) login_manager = LoginManager(app)
login_manager.login_view = 'login' login_manager.login_view = 'login'
@ -423,6 +425,11 @@ def init_app(tr_handler=None):
def index(): def index():
return render_template('index.html') 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']) @app.route('/login', methods=['GET', 'POST'])
def login(): def login():
if request.method == 'POST': if request.method == 'POST':
@ -460,12 +467,14 @@ def init_app(tr_handler=None):
@app.route('/wallet/<int:wallet_id>/transactions', methods=['GET']) @app.route('/wallet/<int:wallet_id>/transactions', methods=['GET'])
@login_required @login_required
@login_required
def get_transactions(wallet_id): def get_transactions(wallet_id):
transactions = storage.get_transactions(wallet_id) transactions = storage.get_transactions(wallet_id)
return jsonify(transactions) return jsonify(transactions)
@app.route('/wallet/<int:wallet_id>/holdings', methods=['GET']) @app.route('/wallet/<int:wallet_id>/holdings', methods=['GET'])
@login_required @login_required
@login_required
def get_holdings(wallet_id): def get_holdings(wallet_id):
holdings = storage.get_holdings(wallet_id) holdings = storage.get_holdings(wallet_id)
return jsonify(holdings) return jsonify(holdings)