Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update arb amounts #6

Merged
merged 5 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "etherfuse-arb"
version = "0.2.0"
version = "0.3.0"
edition = "2021"
license = "Apache-2.0"
description = "A command line interface for transacting arbitrage opportunities on etherfuse."
Expand Down Expand Up @@ -40,3 +40,5 @@ thiserror = "1.0.50"
tokio = { version = "1.39.2", features = ["full"] }
tokio-tungstenite = "0.16"
url = "2.5"
num-derive = "^0.3"
num-traits = "^0.2"
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,5 @@ etherfuse-arb --help
## Example command

```
etherfuse-arb run --keypair ~/.config/solana/id.json \
CETES7CKqqKQizuSN6iWQwmTeFRjbJR6Vw2XRKfEDR8f EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v 1000000
etherfuse-arb run --keypair ~/.config/solana/id.json CETES7CKqqKQizuSN6iWQwmTeFRjbJR6Vw2XRKfEDR8f
```
34 changes: 3 additions & 31 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,11 @@ pub struct JupiterSwapArgs {

#[derive(Parser, Debug, Clone)]
pub struct RunArgs {
#[arg(value_name = "INPUT_MINT", help = "Public key of the input mint")]
pub input_mint: Pubkey,

#[arg(value_name = "OUTPUT_MINT", help = "Public key of the output mint")]
pub output_mint: Pubkey,

#[arg(
value_name = "AMOUNT",
help = "Amount of tokens to swap in token amount"
value_name = "ETHERFUSE_TOKEN",
help = "Public key of the etherfuse token"
)]
pub amount: u64,
pub etherfuse_token: Pubkey,

#[arg(
value_name = "SLIPPAGE_BPS",
Expand All @@ -92,25 +86,3 @@ impl From<JupiterSwapArgs> for JupiterQuoteArgs {
}
}
}

impl From<RunArgs> for JupiterSwapArgs {
fn from(run_args: RunArgs) -> Self {
Self {
input_mint: run_args.input_mint,
output_mint: run_args.output_mint,
amount: run_args.amount,
slippage_bps: run_args.slippage_bps,
}
}
}

impl From<RunArgs> for JupiterQuoteArgs {
fn from(run_args: RunArgs) -> Self {
Self {
input_mint: run_args.input_mint,
output_mint: run_args.output_mint,
amount: run_args.amount,
slippage_bps: run_args.slippage_bps,
}
}
}
4 changes: 4 additions & 0 deletions src/constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub const USDC_MINT: &str = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
pub const USDC_DECIMALS: u8 = 6;
pub const STABLEBOND_DECIMALS: u8 = 6;
pub const MIN_USDC_AMOUNT: u64 = 100000;
3 changes: 1 addition & 2 deletions src/jupiter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,9 @@ impl Arber {
Ok(())
}

pub async fn jupiter_swap_tx(&self, args: JupiterSwapArgs) -> Result<VersionedTransaction> {
pub async fn jupiter_swap_tx(&self, quote: Quote) -> Result<VersionedTransaction> {
let url = format!("{}/swap", self.jupiter_quote_url.as_ref().unwrap());

let quote = self.get_jupiter_quote(args.into()).await?;
let request = SwapRequest {
user_public_key: self.signer().pubkey(),
wrap_and_unwrap_SOL: Some(true),
Expand Down
8 changes: 8 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
mod args;
mod constants;
mod etherfuse;
mod field_as_string;
mod jito;
mod jupiter;
mod math;
mod purchase;
mod run;
mod transaction;
Expand All @@ -18,6 +20,7 @@ use solana_sdk::{
commitment_config::CommitmentConfig,
signature::{read_keypair_file, Keypair},
};

use std::{sync::Arc, sync::RwLock};
use tokio_tungstenite::{connect_async, tungstenite::protocol::Message};

Expand All @@ -28,6 +31,7 @@ struct Arber {
pub jupiter_quote_url: Option<String>,
pub jito_client: HttpClient,
pub jito_tip: Arc<std::sync::RwLock<u64>>,
pub usdc_balance: Arc<std::sync::RwLock<f64>>,
}

#[derive(Subcommand, Debug)]
Expand Down Expand Up @@ -128,6 +132,7 @@ async fn main() -> Result<()> {
let rpc_client = RpcClient::new_with_commitment(cluster, CommitmentConfig::confirmed());
let tip = Arc::new(RwLock::new(0_u64));
let tip_clone = Arc::clone(&tip);
let usdc_balance = Arc::new(RwLock::new(0_f64));

let url = "ws://bundles-api-rest.jito.wtf/api/v1/bundles/tip_stream";
let (ws_stream, _) = connect_async(url).await.unwrap();
Expand Down Expand Up @@ -157,6 +162,7 @@ async fn main() -> Result<()> {
args.jupiter_quote_url,
jito_client,
tip,
usdc_balance,
);

//if the command is test arb and the tip is still 0, we wait until its not
Expand Down Expand Up @@ -190,6 +196,7 @@ impl Arber {
jupiter_quote_url: Option<String>,
jito_client: HttpClient,
jito_tip: Arc<std::sync::RwLock<u64>>,
usdc_balance: Arc<std::sync::RwLock<f64>>,
) -> Self {
Self {
rpc_client,
Expand All @@ -198,6 +205,7 @@ impl Arber {
jupiter_quote_url,
jito_client,
jito_tip,
usdc_balance,
}
}

Expand Down
106 changes: 106 additions & 0 deletions src/math.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#![allow(dead_code)]

use anyhow::{anyhow, Result};
use std::fmt::Display;

pub fn checked_as_f64<T>(arg: T) -> Result<f64>
where
T: Display + num_traits::ToPrimitive + Clone,
{
let option: Option<f64> = num_traits::NumCast::from(arg.clone());
if let Some(res) = option {
Ok(res)
} else {
Err(anyhow!("Math overflow"))
}
}

pub fn checked_as_u64<T>(arg: T) -> Result<u64>
where
T: Display + num_traits::ToPrimitive + Clone,
{
let option: Option<u64> = num_traits::NumCast::from(arg.clone());
if let Some(res) = option {
Ok(res)
} else {
return Err(anyhow!("Math overflow"));
}
}

pub fn checked_div<T>(arg1: T, arg2: T) -> Result<T>
where
T: num_traits::PrimInt + Display,
{
if let Some(res) = arg1.checked_div(&arg2) {
Ok(res)
} else {
return Err(anyhow!("Math overflow"));
}
}

pub fn checked_float_div<T>(arg1: T, arg2: T) -> Result<T>
where
T: num_traits::Float + Display,
{
if arg2 == T::zero() {
return Err(anyhow!("Math overflow"));
}
let res = arg1 / arg2;
if !res.is_finite() {
return Err(anyhow!("Math overflow"));
} else {
Ok(res)
}
}

pub fn checked_mul<T>(arg1: T, arg2: T) -> Result<T>
where
T: num_traits::PrimInt + Display,
{
if let Some(res) = arg1.checked_mul(&arg2) {
Ok(res)
} else {
return Err(anyhow!("Math overflow"));
}
}

pub fn checked_float_mul<T>(arg1: T, arg2: T) -> Result<T>
where
T: num_traits::Float + Display,
{
let res = arg1 * arg2;
if !res.is_finite() {
return Err(anyhow!("Math overflow"));
} else {
Ok(res)
}
}

pub fn checked_powi(arg: f64, exp: i32) -> Result<f64> {
let res = if exp > 0 {
f64::powi(arg, exp)
} else {
// wrokaround due to f64::powi() not working properly on-chain with negative
// exponent
checked_float_div(1.0, f64::powi(arg, -exp))?
};
if res.is_finite() {
Ok(res)
} else {
return Err(anyhow!("Math overflow"));
}
}

pub fn to_ui_amount(amount: u64, decimals: u8) -> Result<f64> {
checked_float_div(
checked_as_f64(amount)?,
checked_powi(10.0, decimals as i32)?,
)
}

pub fn to_token_amount(ui_amount: f64, decimals: u8) -> Result<u64> {
checked_as_u64(checked_float_mul(
ui_amount,
checked_powi(10.0, decimals as i32)?,
)?)
}
87 changes: 71 additions & 16 deletions src/run.rs
Original file line number Diff line number Diff line change
@@ -1,41 +1,96 @@
use crate::args::RunArgs;
use crate::args::{JupiterQuoteArgs, RunArgs};
use crate::constants::{MIN_USDC_AMOUNT, STABLEBOND_DECIMALS, USDC_DECIMALS, USDC_MINT};
use crate::jupiter::Quote;
use crate::math;
use crate::{Arber, PurchaseArgs};

use anyhow::Result;
use solana_sdk::transaction::VersionedTransaction;
use solana_sdk::{pubkey::Pubkey, signer::Signer, transaction::VersionedTransaction};
use spl_associated_token_account::get_associated_token_address;
use std::str::FromStr;

impl Arber {
pub async fn run(&self, args: RunArgs) -> Result<()> {
// run a task that checks arb every 1 minute
let mut interval = tokio::time::interval(std::time::Duration::from_secs(60));
loop {
interval.tick().await;
println!("Checking for arb opportunity");
self.check_arb(args.clone()).await?;
}
}

async fn check_arb(&self, args: RunArgs) -> Result<()> {
let quote = self.get_jupiter_quote(args.clone().into()).await?;
let jup_price_usd_to_token: f64 = quote.in_amount as f64 / quote.out_amount as f64;
let jup_price_token_to_usd: f64 = 1 as f64 / jup_price_usd_to_token;
let etherfuse_price_token_to_usd = self.get_etherfuse_price(args.input_mint).await?;
println!(
"jupiter price: {} \netherfuse price: {}",
jup_price_token_to_usd, etherfuse_price_token_to_usd,
);
if jup_price_token_to_usd > etherfuse_price_token_to_usd {
let usdc_balance = self.update_usdc_balance().await?;
// get etherfuse price of token
let stablebond_price_to_usd = self.get_etherfuse_price(args.etherfuse_token).await?;
let max_usdc_ui_amount_to_purchase = math::checked_float_mul(usdc_balance, 0.99)?;
let mut usdc_token_amount =
math::to_token_amount(max_usdc_ui_amount_to_purchase, USDC_DECIMALS)?;

let stablebond_ui_amount =
math::checked_float_div(max_usdc_ui_amount_to_purchase, stablebond_price_to_usd)?;
let mut stablebond_token_amount =
math::to_token_amount(stablebond_ui_amount, STABLEBOND_DECIMALS)?;
// get jupiter price of token based on quoted amount of USDC in users wallet
let (mut jup_price_token_to_usd, mut quote) = self
.sell_quote(args.clone(), stablebond_token_amount)
.await?;

while usdc_token_amount > MIN_USDC_AMOUNT
&& (jup_price_token_to_usd < stablebond_price_to_usd)
{
// reduce the amount of tokens to purchase to see if arb exists on smaller trade
usdc_token_amount = math::checked_div(usdc_token_amount, 2)?;
stablebond_token_amount = math::checked_div(stablebond_token_amount, 2)?;
(jup_price_token_to_usd, quote) = self
.sell_quote(args.clone(), stablebond_token_amount)
.await?;
}
if usdc_token_amount > MIN_USDC_AMOUNT {
println!("Arb opportunity: jupiter token price > etherfuse price honored. Purchase tokens from etherfuse and sell on jupiter");
let purchase_args = PurchaseArgs {
amount: quote.out_amount,
mint: args.input_mint,
amount: usdc_token_amount,
mint: args.etherfuse_token,
};
let purchase_tx = self.purchase_tx(purchase_args).await?;
let swap_tx = self.jupiter_swap_tx(args.clone().into()).await?;
let swap_tx = self.jupiter_swap_tx(quote).await?;
let txs: &[VersionedTransaction] = &[purchase_tx, swap_tx];
self.send_bundle(txs).await?;
} else {
println!("Arb opportunity: etherfuse price honored < jupiter token price. Purchase tokens from jupiter and sell on etherfuse");
}
Ok(())
}

async fn sell_quote(&self, args: RunArgs, amount: u64) -> Result<(f64, Quote)> {
let jupiter_quote_args = JupiterQuoteArgs {
input_mint: args.etherfuse_token,
output_mint: Pubkey::from_str(USDC_MINT).unwrap(),
amount,
slippage_bps: Some(args.slippage_bps.unwrap_or(300)),
};
let quote = self.get_jupiter_quote(jupiter_quote_args).await?;
let jup_price_usd_to_token: f64 = quote.in_amount as f64 / quote.out_amount as f64;
let jup_price_token_to_usd: f64 = 1 as f64 / jup_price_usd_to_token;
Ok((jup_price_token_to_usd, quote))
}

async fn update_usdc_balance(&self) -> Result<f64> {
let user_usdc_token_account = get_associated_token_address(
&self.signer().pubkey(),
&Pubkey::from_str(USDC_MINT).unwrap(),
);
let token_account = self
.rpc_client
.get_token_account(&user_usdc_token_account)
.await
.expect("unable to get usdc token account");

if let Some(token_account) = token_account {
let usdc_token_account_balance = token_account.token_amount.ui_amount.unwrap();
let mut usdc_balance = self.usdc_balance.write().unwrap();
*usdc_balance = usdc_token_account_balance;
return Ok(usdc_token_account_balance);
}
return Ok(0.0);
}
}