Skip to content

Commit

Permalink
Merge pull request #18 from mrgnlabs/beta/crossbar-patch
Browse files Browse the repository at this point in the history
Feat: Merge swb pull oracle patch
  • Loading branch information
LevBeta authored Sep 6, 2024
2 parents a26cf16 + 7eea6f1 commit 3a247aa
Show file tree
Hide file tree
Showing 20 changed files with 1,112 additions and 267 deletions.
455 changes: 430 additions & 25 deletions Cargo.lock

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,16 @@ bytemuck = "1.14.0"
bytes = "1.5.0"
clap = { version = "4.5.4", features = ["derive"] }
crossbeam = { version = "0.8.4", features = ["crossbeam-channel"] }
dirs = "4.0.0"
env_logger = "0.11.3"
fixed = "1.24.0"
fixed-macro = "1.2.0"
futures = "0.3.30"
futures-sink = "0.3.30"
jupiter-swap-api-client = "0.1.0"
lazy_static = "1.5.0"
log = "0.4.21"
marginfi = { git = "https://github.com/mrgnlabs/marginfi-v2", branch = "main", features = [
marginfi = { git = "https://github.com/mrgnlabs/marginfi-v2", branch = "man0s/crossbar-legacy-indexer", features = [
"mainnet-beta",
"client",
"no-entrypoint",
Expand Down Expand Up @@ -56,6 +58,11 @@ yellowstone-grpc-client = { git = "https://github.com/mrgnlabs/yellowstone-grpc"
yellowstone-grpc-proto = { git = "https://github.com/mrgnlabs/yellowstone-grpc", branch = "1.18.17" }
jito-protos = { git = "https://github.com/mrgnlabs/jito-rs", branch = "1.18.17" }
jito-searcher-client = { git = "https://github.com/mrgnlabs/jito-rs", branch = "1.18.17" }
switchboard-on-demand = "0.1.7"
switchboard-on-demand-client = "0.1.7"
chrono = "0.4.38"
hex = "0.4.3"
url = "2.5.2"

[profile.release]
opt-level = 3
Expand Down
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,7 @@ WORKDIR /app
# Copy the build artifact from the build stage
COPY --from=builder /usr/src/app/target/release/eva01 .

ENV RUST_LOG=eva01=info

# Set the startup command
CMD ["./eva01", "run", "/config/config.toml"]
CMD ["./eva01", "run", "/config/config.toml"]
2 changes: 1 addition & 1 deletion src/cli/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ pub struct SetupFromCliOpts {
#[arg(short = 'u', long, help = "RPC endpoint url")]
pub rpc_url: String,
#[arg(short = 'k', long, help = "Signer keypair path")]
pub keypair_path: String,
pub keypair_path: PathBuf,
#[arg(
long,
help = "Signer pubkey, if not provided, will be derived from the keypair"
Expand Down
147 changes: 72 additions & 75 deletions src/cli/setup/mod.rs
Original file line number Diff line number Diff line change
@@ -1,75 +1,96 @@
use crate::config::{Eva01Config, GeneralConfig, LiquidatorCfg, RebalancerCfg};
use super::app::SetupFromCliOpts;
use crate::{
config::{Eva01Config, GeneralConfig, LiquidatorCfg, RebalancerCfg},
utils::{ask_keypair_until_valid, expand_tilde, is_valid_url, prompt_user},
};

use anyhow::bail;
use fixed::types::I80F48;
use lazy_static::lazy_static;
use solana_account_decoder::{UiAccountEncoding, UiDataSliceConfig};
use solana_client::rpc_client::RpcClient;
use solana_client::{
rpc_client::RpcClient,
rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig},
rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType},
};
use solana_program::pubkey::Pubkey;
use solana_sdk::{
signature::{read_keypair_file, Signer},
signer::keypair::Keypair,
};
use std::io::Write;
use std::path::PathBuf;

use super::app::SetupFromCliOpts;
use solana_sdk::signature::{read_keypair_file, Signer};
use std::{ops::Not, path::PathBuf, str::FromStr};

/// Helper for initializing Marginfi Account
pub mod initialize;

/// 1º -> Ask for path where the config file will be stored
///
/// 2º -> Ask for a solana RPC endpoint url
/// -> Verify if we have access to the RPC endpoint
///
/// 3º -> Ask for the signer keypair path
/// -> Check if the MarginfiAccount is already initialized in that keypair
/// If not, try to initialize a account
///
/// 4º -> Ask for the yellowstone rpc and optinal x token
lazy_static! {
static ref DEFAULT_CONFIG_PATH: PathBuf = {
let mut path = dirs::home_dir().expect("Couldn't find the config directory");
path.push(".config");
path.push("eva01");

path
};
}

pub async fn setup() -> anyhow::Result<()> {
// 1º Step
let configuration_path = PathBuf::from(
prompt_user("Pretended configuration file location\nExample: /home/mrgn/.config/liquidator/config.toml\n> ")?
);
// Config location
let input_raw = prompt_user(&format!(
"Select config location [default: {:?}]: ",
*DEFAULT_CONFIG_PATH
))?;
let configuration_dir = if input_raw.is_empty() {
DEFAULT_CONFIG_PATH.clone()
} else {
expand_tilde(&input_raw)
};
if !configuration_dir.exists() {
std::fs::create_dir_all(&configuration_dir)?;
}
let configuration_path = configuration_dir.join("config.toml");

// 2º Step
let rpc_url = prompt_user("RPC endpoint url\n> ")?;
// RPC config
let rpc_url = prompt_user("RPC endpoint url [required]: ")?;
if !is_valid_url(&rpc_url) {
bail!("Invalid RPC endpoint");
}
let rpc_client = RpcClient::new(rpc_url.clone());

// 3º Step
// Target program/group
let input_raw = prompt_user(&format!(
"Select marginfi program [default: {:?}]: ",
GeneralConfig::default_marginfi_program_id()
))?;
let marginfi_program_id = if input_raw.is_empty() {
GeneralConfig::default_marginfi_program_id()
} else {
Pubkey::from_str(&input_raw).expect("Invalid marginfi program id")
};

let input_raw = prompt_user(&format!(
"Select marginfi group [default: {:?}]: ",
GeneralConfig::default_marginfi_group_address()
))?;
let marginfi_group_address = if input_raw.is_empty() {
GeneralConfig::default_marginfi_group_address()
} else {
Pubkey::from_str(&input_raw).expect("Invalid marginfi group address")
};

// Marginfi account discovery/selection
let (keypair_path, signer_keypair) = ask_keypair_until_valid()?;
let accounts = marginfi_account_by_authority(signer_keypair.pubkey(), rpc_client).await?;
if accounts.is_empty() {
let create_new =
prompt_user("There is no marginfi account \nDo you wish to create a new one? Y/n\n> ")?
.as_str()
!= "n";
if !create_new {
println!("Can't proceed without a marginfi account.");
return Err(anyhow::anyhow!("Can't proceed without a marginfi account."));
}
// Initialize a marginfi account
println!("No marginfi account found for the provided signer. Please create one first.");
bail!("No marginfi account found");
// TODO: initialize a marginfi account programmatically
}

// 4º step
let yellowstone_endpoint = prompt_user("Yellowstone endpoint url\n> ")?;

let yellowstone_endpoint = prompt_user("Yellowstone endpoint url [required]: ")?;
let yellowstone_x_token = {
let x_token =
prompt_user("Do you wish to add yellowstone x token? \nPress enter if not\n> ")?;

if x_token.is_empty() {
None
} else {
Some(x_token)
}
let x_token = prompt_user("Yellowstone x-token [optional]: ")?;
x_token.is_empty().not().then_some(x_token)
};

let isolated_banks =
prompt_user("Do you wish to liquidate on isolated banks? Y/n\n> ")?.to_lowercase() == "y";
prompt_user("Enable isolated banks liquidation? [Y/n] ")?.to_lowercase() == "y";

let general_config = GeneralConfig {
rpc_url,
Expand All @@ -81,8 +102,8 @@ pub async fn setup() -> anyhow::Result<()> {
liquidator_account: accounts[0],
compute_unit_price_micro_lamports: GeneralConfig::default_compute_unit_price_micro_lamports(
),
marginfi_program_id: GeneralConfig::default_marginfi_program_id(),
marginfi_group_address: GeneralConfig::default_marginfi_group_address(),
marginfi_program_id,
marginfi_group_address,
account_whitelist: GeneralConfig::default_account_whitelist(),
address_lookup_tables: GeneralConfig::default_address_lookup_tables(),
};
Expand Down Expand Up @@ -152,7 +173,7 @@ pub async fn setup_from_cfg(
Some(pubkey) => pubkey,
None => {
let signer_keypair = read_keypair_file(&keypair_path)
.unwrap_or_else(|_| panic!("Failed to read keypair from path: {}", keypair_path));
.unwrap_or_else(|_| panic!("Failed to read keypair from path: {:?}", keypair_path));
signer_keypair.pubkey()
}
};
Expand Down Expand Up @@ -223,30 +244,6 @@ pub async fn setup_from_cfg(
Ok(())
}

fn prompt_user(prompt_text: &str) -> anyhow::Result<String> {
print!("{}", prompt_text);
let mut input = String::new();
std::io::stdout().flush()?;
std::io::stdin().read_line(&mut input)?;
input.pop();
Ok(input)
}

/// Simply asks the keypair path until it is a valid one,
/// Returns (keypair_path, signer_keypair)
fn ask_keypair_until_valid() -> anyhow::Result<(String, Keypair)> {
println!("Keypair file path");
loop {
let keypair_path = prompt_user("> ")?;
match read_keypair_file(&keypair_path) {
Ok(keypair) => return Ok((keypair_path, keypair)),
Err(_) => {
println!("Failed to load the keypair from the provided path. Please try again");
}
}
}
}

async fn marginfi_account_by_authority(
authority: Pubkey,
rpc_client: RpcClient,
Expand Down
4 changes: 2 additions & 2 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ pub struct GeneralConfig {
serialize_with = "pubkey_to_str"
)]
pub signer_pubkey: Pubkey,
pub keypair_path: String,
pub keypair_path: PathBuf,
#[serde(
deserialize_with = "from_pubkey_string",
serialize_with = "pubkey_to_str"
Expand Down Expand Up @@ -99,7 +99,7 @@ impl std::fmt::Display for GeneralConfig {
- Yellowstone Endpoint: {}\n\
- Yellowstone X Token: {}\n\
- Signer Pubkey: {}\n\
- Keypair Path: {}\n\
- Keypair Path: {:?}\n\
- Liquidator Account: {}\n\
- Compute Unit Price Micro Lamports: {}\n\
- Marginfi Program ID: {}\n\
Expand Down
70 changes: 70 additions & 0 deletions src/crossbar.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use solana_sdk::pubkey::Pubkey;
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
use switchboard_on_demand_client::CrossbarClient;

/// CrossbarMaintainer will maintain the feeds prices
/// with simulated prices from the crossbar service
pub(crate) struct CrossbarMaintainer {
crossbar_client: CrossbarClient,
}

impl CrossbarMaintainer {
/// Creates a new CrossbarMaintainer empty instance
pub fn new() -> Self {
let crossbar_client = CrossbarClient::default(None);
Self { crossbar_client }
}

pub async fn simulate(&self, feeds: Vec<(Pubkey, String)>) -> Vec<(Pubkey, f64)> {
if feeds.is_empty() {
return Vec::new();
}

// Create a fast lookup map from feed hash to oracle hash
let feed_hash_to_oracle_hash_map: HashMap<String, Pubkey> = feeds
.iter()
.map(|(address, feed_hash)| (feed_hash.clone(), address.clone()))
.collect();

let feed_hashes: Vec<&str> = feeds
.iter()
.map(|(_, feed_hash)| feed_hash.as_str())
.collect();

let simulated_prices = self
.crossbar_client
.simulate_feeds(&feed_hashes)
.await
.unwrap();
let mut prices = Vec::new();
for simulated_response in simulated_prices {
if let Some(price) = calculate_price(simulated_response.results) {
prices.push((
feed_hash_to_oracle_hash_map
.get(&simulated_response.feedHash)
.unwrap()
.clone(),
price,
));
}
}
prices
}
}
fn calculate_price(mut numbers: Vec<f64>) -> Option<f64> {
if numbers.is_empty() {
return None;
}

numbers.sort_by(|a, b| a.partial_cmp(b).unwrap());
let mid = numbers.len() / 2;

if numbers.len() % 2 == 0 {
Some((numbers[mid - 1] + numbers[mid]) / 2.0)
} else {
Some(numbers[mid])
}
}
3 changes: 1 addition & 2 deletions src/geyser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use anchor_lang::AccountDeserialize;
use crossbeam::channel::Sender;
use futures::StreamExt;
use log::{error, info};
use marginfi::{instructions::marginfi_account, state::marginfi_account::MarginfiAccount};
use marginfi::state::marginfi_account::MarginfiAccount;
use solana_program::pubkey::Pubkey;
use solana_sdk::account::Account;
use std::{collections::HashMap, mem::size_of};
Expand Down Expand Up @@ -170,7 +170,6 @@ impl GeyserService {
}
}
}
Ok(())
}

/// Builds a geyser subscription request payload
Expand Down
Loading

0 comments on commit 3a247aa

Please sign in to comment.