use std::{ str::FromStr, sync::{ atomic::{AtomicBool, AtomicU64, Ordering}, Arc, }, time::{Duration, Instant}, }; use chrono::Utc; use iter_tools::Itertools; use log::{debug, info, warn}; use mango::{ instruction::{cancel_all_perp_orders, place_perp_order2}, matching::Side, }; use rand::{distributions::Uniform, prelude::Distribution, seq::SliceRandom}; use solana_client::nonblocking::rpc_client::RpcClient; use solana_program::pubkey::Pubkey; use solana_sdk::{ compute_budget, hash::Hash, instruction::Instruction, message::Message, signature::Keypair, signer::Signer, transaction::Transaction, }; use tokio::{ sync::RwLock, task::{self, JoinHandle}, }; use crate::{ helpers::{to_sdk_instruction, to_sp_pk}, mango::AccountKeys, states::{PerpMarketCache, TransactionSendRecord}, tpu_manager::TpuManager, }; pub fn create_ask_bid_transaction( c: &PerpMarketCache, mango_account_pk: Pubkey, mango_account_signer: &Keypair, prioritization_fee: u64, ) -> Transaction { let mango_account_signer_pk = to_sp_pk(&mango_account_signer.pubkey()); let offset = rand::random::() as i64; let spread = rand::random::() as i64; debug!( "price:{:?} price_quote_lots:{:?} order_base_lots:{:?} offset:{:?} spread:{:?}", c.price, c.price_quote_lots, c.order_base_lots, offset, spread ); let mut instructions = vec![]; if prioritization_fee > 0 { let pfees = compute_budget::ComputeBudgetInstruction::set_compute_unit_price(prioritization_fee); instructions.push(pfees); } let cancel_ix: Instruction = to_sdk_instruction( cancel_all_perp_orders( &c.mango_program_pk, &c.mango_group_pk, &mango_account_pk, &mango_account_signer_pk, &c.perp_market_pk, &c.perp_market.bids, &c.perp_market.asks, 10, ) .unwrap(), ); instructions.push(cancel_ix); let place_bid_ix: Instruction = to_sdk_instruction( place_perp_order2( &c.mango_program_pk, &c.mango_group_pk, &mango_account_pk, &mango_account_signer_pk, &c.mango_cache_pk, &c.perp_market_pk, &c.perp_market.bids, &c.perp_market.asks, &c.perp_market.event_queue, None, &[], Side::Bid, c.price_quote_lots + offset - spread, c.order_base_lots, i64::MAX, Utc::now().timestamp_micros() as u64, mango::matching::OrderType::Limit, false, None, 64, mango::matching::ExpiryType::Absolute, ) .unwrap(), ); instructions.push(place_bid_ix); let place_ask_ix: Instruction = to_sdk_instruction( place_perp_order2( &c.mango_program_pk, &c.mango_group_pk, &mango_account_pk, &mango_account_signer_pk, &c.mango_cache_pk, &c.perp_market_pk, &c.perp_market.bids, &c.perp_market.asks, &c.perp_market.event_queue, None, &[], Side::Ask, c.price_quote_lots + offset + spread, c.order_base_lots, i64::MAX, Utc::now().timestamp_micros() as u64, mango::matching::OrderType::Limit, false, None, 64, mango::matching::ExpiryType::Absolute, ) .unwrap(), ); instructions.push(place_ask_ix); Transaction::new_unsigned(Message::new( instructions.as_slice(), Some(&mango_account_signer.pubkey()), )) } fn generate_random_fees( prioritization_fee_proba: u8, n: usize, min_fee: u64, max_fee: u64, ) -> Vec { let mut rng = rand::thread_rng(); let range = Uniform::from(min_fee..max_fee); let range_probability = Uniform::from(1..100); (0..n) .map(|_| { if prioritization_fee_proba == 0 { 0 } else { if range_probability.sample(&mut rng) <= prioritization_fee_proba { range.sample(&mut rng) as u64 } else { 0 } } }) .collect() } pub async fn send_mm_transactions( quotes_per_second: u64, perp_market_caches: &Vec, tpu_manager: TpuManager, mango_account_pk: Pubkey, mango_account_signer: &Keypair, blockhash: Arc>, slot: &AtomicU64, prioritization_fee_proba: u8, ) { let mango_account_signer_pk = to_sp_pk(&mango_account_signer.pubkey()); // update quotes 2x per second for _ in 0..quotes_per_second { let prioritization_fee_by_market = generate_random_fees( prioritization_fee_proba, perp_market_caches.len(), 100, 1000, ); let mut batch_to_send = Vec::with_capacity(perp_market_caches.len()); for (i, c) in perp_market_caches.iter().enumerate() { let prioritization_fee = prioritization_fee_by_market[i]; let mut tx = create_ask_bid_transaction( c, mango_account_pk, &mango_account_signer, prioritization_fee, ); let recent_blockhash = *blockhash.read().await; tx.sign(&[mango_account_signer], recent_blockhash); let record = TransactionSendRecord { signature: tx.signatures[0], sent_at: Utc::now(), sent_slot: slot.load(Ordering::Acquire), market_maker: Some(mango_account_signer_pk), market: Some(c.perp_market_pk), priority_fees: prioritization_fee, keeper_instruction: None, }; batch_to_send.push((tx, record)); } let tpu_manager = tpu_manager.clone(); task::spawn(async move { if !tpu_manager.send_transaction_batch(&batch_to_send).await { println!("sending failed on tpu client"); } }); } } pub fn start_market_making_threads( account_keys_parsed: Vec, perp_market_caches: Vec, exit_signal: Arc, blockhash: Arc>, current_slot: Arc, tpu_manager: TpuManager, duration: &Duration, quotes_per_second: u64, prioritization_fee_proba: u8, number_of_markers_per_mm: u8, ) -> Vec> { let mut rng = rand::thread_rng(); account_keys_parsed .iter() .map(|account_keys| { let exit_signal = exit_signal.clone(); let blockhash = blockhash.clone(); let current_slot = current_slot.clone(); let duration = duration.clone(); let perp_market_caches = perp_market_caches.clone(); let mango_account_pk = Pubkey::from_str(account_keys.mango_account_pks[0].as_str()).unwrap(); let mango_account_signer = Keypair::from_bytes(account_keys.secret_key.as_slice()).unwrap(); let tpu_manager = tpu_manager.clone(); info!( "wallet: {:?} mango account: {:?}", mango_account_signer.pubkey(), mango_account_pk ); let perp_market_caches = perp_market_caches .choose_multiple(&mut rng, number_of_markers_per_mm as usize) .map(|x| x.clone()) .collect_vec(); tokio::spawn(async move { for _i in 0..duration.as_secs() { if exit_signal.load(Ordering::Relaxed) { break; } let start = Instant::now(); // send market maker transactions send_mm_transactions( quotes_per_second, &perp_market_caches, tpu_manager.clone(), mango_account_pk, &mango_account_signer, blockhash.clone(), current_slot.as_ref(), prioritization_fee_proba, ) .await; let elapsed_millis: u64 = start.elapsed().as_millis() as u64; if elapsed_millis < 1000 { tokio::time::sleep(Duration::from_millis(1000 - elapsed_millis)).await; } else { warn!( "time taken to send transactions is greater than 1000ms {}", elapsed_millis ); } } }) }) .collect() } fn create_cancel_all_orders( perp_market: &PerpMarketCache, mango_account_pk: Pubkey, mango_account_signer: &Keypair, ) -> Transaction { let mango_account_signer_pk = to_sp_pk(&mango_account_signer.pubkey()); let cb_instruction = compute_budget::ComputeBudgetInstruction::set_compute_unit_limit(1000000); let pf_instruction = compute_budget::ComputeBudgetInstruction::set_compute_unit_price(1000); let instruction: Instruction = to_sdk_instruction( cancel_all_perp_orders( &perp_market.mango_program_pk, &perp_market.mango_group_pk, &mango_account_pk, &mango_account_signer_pk, &perp_market.perp_market_pk, &perp_market.bids, &perp_market.asks, 255, ) .unwrap(), ); Transaction::new_unsigned(Message::new( &[cb_instruction, pf_instruction, instruction], Some(&mango_account_signer.pubkey()), )) } pub async fn clean_market_makers( rpc_client: Arc, account_keys_parsed: &Vec, perp_market_caches: &Vec, blockhash: Arc>, ) { info!("Cleaning previous transactions by market makers"); for account_keys_parsed in account_keys_parsed.chunks(10) { let mut tasks = vec![]; for market_maker in account_keys_parsed { let mango_account_pk = Pubkey::from_str(market_maker.mango_account_pks[0].as_str()).unwrap(); for perp_market in perp_market_caches { let market_maker = market_maker.clone(); let perp_market = perp_market.clone(); let rpc_client = rpc_client.clone(); let mango_account_pk = mango_account_pk.clone(); let blockhash = blockhash.clone(); let task = tokio::spawn(async move { let mango_account_signer = Keypair::from_bytes(market_maker.secret_key.as_slice()).unwrap(); for _ in 0..10 { let mut tx = create_cancel_all_orders( &perp_market, mango_account_pk, &mango_account_signer, ); let recent_blockhash = *blockhash.read().await; tx.sign(&[&mango_account_signer], recent_blockhash); let sig = tx.signatures[0]; // send and confirm the transaction with an RPC if let Ok(res) = tokio::time::timeout( Duration::from_secs(10), rpc_client.send_and_confirm_transaction(&tx), ) .await { match res { Ok(_) => break, Err(e) => info!("Error occured while doing cancel all for ma : {}, sig : {} perp market : {} error : {}", mango_account_pk, sig, perp_market.perp_market_pk, e), } } } }); tasks.push(task); } } futures::future::join_all(tasks).await; } info!("finished cleaning market makers"); }