From b1b39d356966f1ad0af27926581e0f9f8efc4bf9 Mon Sep 17 00:00:00 2001
From: calintje
Date: Sat, 30 Nov 2024 04:36:16 +0100
Subject: [PATCH 01/18] Add base repositioning logic
---
.gitignore | 1 +
Cargo.toml | 3 +-
examples/lp-bot/Cargo.toml | 14 ++++
examples/lp-bot/src/main.rs | 124 ++++++++++++++++++++++++++++++++++++
4 files changed, 141 insertions(+), 1 deletion(-)
create mode 100644 examples/lp-bot/Cargo.toml
create mode 100644 examples/lp-bot/src/main.rs
diff --git a/.gitignore b/.gitignore
index fe9c4620..0f89ceda 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,3 +27,4 @@ generated
**/Cargo.lock
.next
.env
+wallet.json
diff --git a/Cargo.toml b/Cargo.toml
index e32be758..8a8f1231 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,5 +6,6 @@ members = [
exclude = [
"rust-sdk",
"ts-sdk",
- "docs"
+ "docs",
+ "examples"
]
diff --git a/examples/lp-bot/Cargo.toml b/examples/lp-bot/Cargo.toml
new file mode 100644
index 00000000..ce918a34
--- /dev/null
+++ b/examples/lp-bot/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "lp-bot"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+orca_whirlpools = "0.4.0"
+orca_whirlpools_client = "0.4.3"
+orca_whirlpools_core = "0.4.3"
+serde_json = "1.0.133"
+solana-client = "1.18"
+solana-sdk = "1.18"
+spl-token-2022 = { version = "^3.0" }
+tokio = "1.41.1"
diff --git a/examples/lp-bot/src/main.rs b/examples/lp-bot/src/main.rs
new file mode 100644
index 00000000..6c058a82
--- /dev/null
+++ b/examples/lp-bot/src/main.rs
@@ -0,0 +1,124 @@
+use orca_whirlpools::{
+ close_position_instructions, open_position_instructions, set_funder,
+ set_whirlpools_config_address, IncreaseLiquidityParam, WhirlpoolsConfigInput,
+};
+use orca_whirlpools_client::{get_position_address, Position, Whirlpool};
+use orca_whirlpools_core::{sqrt_price_to_price, tick_index_to_price};
+use solana_client::nonblocking::rpc_client::RpcClient;
+use solana_sdk::message::{self, Message};
+use solana_sdk::program_pack::Pack;
+use solana_sdk::pubkey::Pubkey;
+use solana_sdk::transaction::Transaction;
+use solana_sdk::{signature::Keypair, signer::Signer};
+use spl_token_2022::state::Mint;
+use std::str::FromStr;
+use std::{fs, vec};
+
+#[tokio::main]
+async fn main() {
+ set_whirlpools_config_address(WhirlpoolsConfigInput::SolanaMainnet).unwrap();
+ let rpc = RpcClient::new("https://api.mainnet-beta.solana.com".to_string());
+ let wallet = load_wallet();
+ set_funder(wallet.pubkey()).unwrap();
+
+ let position_mint_address =
+ Pubkey::from_str("").unwrap();
+
+ let (position_address, _) = get_position_address(&position_mint_address).unwrap();
+ let position_account = rpc.get_account(&position_address).await.unwrap();
+ let position: Position = Position::from_bytes(&position_account.data).unwrap();
+
+ let whirlpool_address = position.whirlpool;
+ let whirlpool_account = rpc.get_account(&whirlpool_address).await.unwrap();
+ let whirlpool = Whirlpool::from_bytes(&whirlpool_account.data).unwrap();
+
+ let token_mint_a_address = whirlpool.token_mint_a;
+ let token_mint_a_account = rpc.get_account(&token_mint_a_address).await.unwrap();
+ let token_mint_a = Mint::unpack(&token_mint_a_account.data).unwrap();
+ let decimals_a = token_mint_a.decimals;
+
+ let token_mint_b_address = whirlpool.token_mint_b;
+ let token_mint_b_account = rpc.get_account(&token_mint_b_address).await.unwrap();
+ let token_mint_b = Mint::unpack(&token_mint_b_account.data).unwrap();
+ let decimals_b = token_mint_b.decimals;
+
+ let current_price = sqrt_price_to_price(whirlpool.sqrt_price, decimals_a, decimals_b);
+ let position_lower_price =
+ tick_index_to_price(position.tick_lower_index, decimals_a, decimals_b);
+ let position_upper_price =
+ tick_index_to_price(position.tick_upper_index, decimals_a, decimals_b);
+ let position_center_price = (position_lower_price + position_upper_price) / 2.0;
+
+ let threshold = 1.0;
+ let deviation = ((current_price - position_center_price).abs() / position_center_price) * 100.0;
+ println!("Current pool price: {:.6}", current_price);
+ println!("Position price range: [{:.6}, {:.6}]", position_lower_price, position_upper_price);
+ println!("Position center price: {:.6}", position_center_price);
+ println!("Threshold for repositioning: {:.2}%", threshold);
+ println!("Price deviation from center: {:.2}%", deviation);
+
+ if deviation >= threshold {
+ println!("Deviation exceeds threshold. Closing position...");
+ let close_position_instructions =
+ close_position_instructions(&rpc, position_mint_address, Some(100), None)
+ .await
+ .unwrap();
+ let recent_blockhash = rpc.get_latest_blockhash().await.unwrap();
+ let message = Message::new(
+ &close_position_instructions.instructions,
+ Some(&wallet.pubkey()),
+ );
+ let transaction = Transaction::new(&vec![wallet.as_ref()], message, recent_blockhash);
+ let signature = rpc
+ .send_and_confirm_transaction(&transaction)
+ .await
+ .unwrap();
+ println!("Close position transaction signature: {}", signature);
+
+ let new_lower_price = current_price - (position_upper_price - position_lower_price) / 2.0;
+ let new_upper_price = current_price + (position_upper_price - position_lower_price) / 2.0;
+ println!("New position price range: [{:.6}, {:.6}]", new_lower_price, new_upper_price);
+ println!("Opening new position with adjusted range...");
+ let increase_liquidity_param =
+ IncreaseLiquidityParam::Liquidity(close_position_instructions.quote.liquidity_delta);
+ let open_position_instructions = open_position_instructions(
+ &rpc,
+ whirlpool_address,
+ new_lower_price,
+ new_upper_price,
+ increase_liquidity_param,
+ Some(100),
+ None,
+ )
+ .await
+ .unwrap();
+ let recent_blockhash = rpc.get_latest_blockhash().await.unwrap();
+ let message = Message::new(
+ &open_position_instructions.instructions,
+ Some(&wallet.pubkey()),
+ );
+ let mut signers: Vec<&dyn Signer> = vec![wallet.as_ref()];
+ signers.extend(
+ open_position_instructions
+ .additional_signers
+ .iter()
+ .map(|kp| kp as &dyn Signer),
+ );
+ let transaction = Transaction::new(&signers, message, recent_blockhash);
+ let signature = rpc
+ .send_and_confirm_transaction(&transaction)
+ .await
+ .unwrap();
+ println!("Open position transaction signature: {}", signature);
+ println!("New position mint address: {}", open_position_instructions.position_mint);
+ } else {
+ println!("Current price is within range. No repositioning needed.");
+ }
+}
+
+fn load_wallet() -> Box {
+ let wallet_string = fs::read_to_string("wallet.json").unwrap();
+ let keypair_bytes: Vec = serde_json::from_str(&wallet_string).unwrap();
+ let wallet = Keypair::from_bytes(&keypair_bytes).unwrap();
+ Box::new(wallet)
+}
From cdf95a7fe8e6f1919f40ef1e824e085c8c920357 Mon Sep 17 00:00:00 2001
From: calintje
Date: Sat, 30 Nov 2024 05:09:38 +0100
Subject: [PATCH 02/18] Add clap for cli input. Add loop.
---
examples/lp-bot/Cargo.toml | 1 +
examples/lp-bot/src/main.rs | 188 +++++++++++++++++++++---------------
2 files changed, 109 insertions(+), 80 deletions(-)
diff --git a/examples/lp-bot/Cargo.toml b/examples/lp-bot/Cargo.toml
index ce918a34..03249634 100644
--- a/examples/lp-bot/Cargo.toml
+++ b/examples/lp-bot/Cargo.toml
@@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2021"
[dependencies]
+clap = { version = "4.5.21", features = ["derive"] }
orca_whirlpools = "0.4.0"
orca_whirlpools_client = "0.4.3"
orca_whirlpools_core = "0.4.3"
diff --git a/examples/lp-bot/src/main.rs b/examples/lp-bot/src/main.rs
index 6c058a82..4750d6ae 100644
--- a/examples/lp-bot/src/main.rs
+++ b/examples/lp-bot/src/main.rs
@@ -1,3 +1,4 @@
+use clap::Parser;
use orca_whirlpools::{
close_position_instructions, open_position_instructions, set_funder,
set_whirlpools_config_address, IncreaseLiquidityParam, WhirlpoolsConfigInput,
@@ -13,106 +14,133 @@ use solana_sdk::{signature::Keypair, signer::Signer};
use spl_token_2022::state::Mint;
use std::str::FromStr;
use std::{fs, vec};
+use tokio::time::{sleep, Duration};
+
+#[derive(Parser)]
+#[command(author, version, about, long_about = None)]
+struct Args {
+ /// Position mint address
+ #[arg(short, long)]
+ position_mint_address: String,
+
+ /// Threshold for repositioning (in percentage)
+ #[arg(short, long, default_value_t = 1.0)]
+ threshold: f64,
+
+ /// Time interval for checking (in seconds)
+ #[arg(short, long, default_value_t = 60)]
+ interval: u64,
+}
#[tokio::main]
async fn main() {
+ let args = Args::parse();
+
set_whirlpools_config_address(WhirlpoolsConfigInput::SolanaMainnet).unwrap();
let rpc = RpcClient::new("https://api.mainnet-beta.solana.com".to_string());
let wallet = load_wallet();
set_funder(wallet.pubkey()).unwrap();
- let position_mint_address =
- Pubkey::from_str("").unwrap();
+ let mut position_mint_address = Pubkey::from_str(&args.position_mint_address).unwrap();
+ let threshold = args.threshold;
+ let interval = args.interval;
+
+ println!("Position Mint Address: {}", position_mint_address);
+ println!("Threshold: {:.2}%", threshold);
+ println!("Interval: {} seconds", interval);
- let (position_address, _) = get_position_address(&position_mint_address).unwrap();
- let position_account = rpc.get_account(&position_address).await.unwrap();
- let position: Position = Position::from_bytes(&position_account.data).unwrap();
+ loop {
+ println!("Checking position...");
+ let (position_address, _) = get_position_address(&position_mint_address).unwrap();
+ let position_account = rpc.get_account(&position_address).await.unwrap();
+ let position: Position = Position::from_bytes(&position_account.data).unwrap();
- let whirlpool_address = position.whirlpool;
- let whirlpool_account = rpc.get_account(&whirlpool_address).await.unwrap();
- let whirlpool = Whirlpool::from_bytes(&whirlpool_account.data).unwrap();
+ let whirlpool_address = position.whirlpool;
+ let whirlpool_account = rpc.get_account(&whirlpool_address).await.unwrap();
+ let whirlpool = Whirlpool::from_bytes(&whirlpool_account.data).unwrap();
- let token_mint_a_address = whirlpool.token_mint_a;
- let token_mint_a_account = rpc.get_account(&token_mint_a_address).await.unwrap();
- let token_mint_a = Mint::unpack(&token_mint_a_account.data).unwrap();
- let decimals_a = token_mint_a.decimals;
+ let token_mint_a_address = whirlpool.token_mint_a;
+ let token_mint_a_account = rpc.get_account(&token_mint_a_address).await.unwrap();
+ let token_mint_a = Mint::unpack(&token_mint_a_account.data).unwrap();
+ let decimals_a = token_mint_a.decimals;
- let token_mint_b_address = whirlpool.token_mint_b;
- let token_mint_b_account = rpc.get_account(&token_mint_b_address).await.unwrap();
- let token_mint_b = Mint::unpack(&token_mint_b_account.data).unwrap();
- let decimals_b = token_mint_b.decimals;
+ let token_mint_b_address = whirlpool.token_mint_b;
+ let token_mint_b_account = rpc.get_account(&token_mint_b_address).await.unwrap();
+ let token_mint_b = Mint::unpack(&token_mint_b_account.data).unwrap();
+ let decimals_b = token_mint_b.decimals;
- let current_price = sqrt_price_to_price(whirlpool.sqrt_price, decimals_a, decimals_b);
- let position_lower_price =
- tick_index_to_price(position.tick_lower_index, decimals_a, decimals_b);
- let position_upper_price =
- tick_index_to_price(position.tick_upper_index, decimals_a, decimals_b);
- let position_center_price = (position_lower_price + position_upper_price) / 2.0;
+ let current_price = sqrt_price_to_price(whirlpool.sqrt_price, decimals_a, decimals_b);
+ let position_lower_price =
+ tick_index_to_price(position.tick_lower_index, decimals_a, decimals_b);
+ let position_upper_price =
+ tick_index_to_price(position.tick_upper_index, decimals_a, decimals_b);
+ let position_center_price = (position_lower_price + position_upper_price) / 2.0;
- let threshold = 1.0;
- let deviation = ((current_price - position_center_price).abs() / position_center_price) * 100.0;
- println!("Current pool price: {:.6}", current_price);
- println!("Position price range: [{:.6}, {:.6}]", position_lower_price, position_upper_price);
- println!("Position center price: {:.6}", position_center_price);
- println!("Threshold for repositioning: {:.2}%", threshold);
- println!("Price deviation from center: {:.2}%", deviation);
+ let deviation = ((current_price - position_center_price).abs() / position_center_price) * 100.0;
+ println!("Current pool price: {:.6}", current_price);
+ println!("Position price range: [{:.6}, {:.6}]", position_lower_price, position_upper_price);
+ println!("Position center price: {:.6}", position_center_price);
+ println!("Price deviation from center: {:.2}%", deviation);
- if deviation >= threshold {
- println!("Deviation exceeds threshold. Closing position...");
- let close_position_instructions =
- close_position_instructions(&rpc, position_mint_address, Some(100), None)
+ if deviation >= threshold {
+ println!("Deviation exceeds threshold. Closing position...");
+ let close_position_instructions =
+ close_position_instructions(&rpc, position_mint_address, Some(100), None)
+ .await
+ .unwrap();
+ let recent_blockhash = rpc.get_latest_blockhash().await.unwrap();
+ let message = Message::new(
+ &close_position_instructions.instructions,
+ Some(&wallet.pubkey()),
+ );
+ let transaction = Transaction::new(&vec![wallet.as_ref()], message, recent_blockhash);
+ let signature = rpc
+ .send_and_confirm_transaction(&transaction)
.await
.unwrap();
- let recent_blockhash = rpc.get_latest_blockhash().await.unwrap();
- let message = Message::new(
- &close_position_instructions.instructions,
- Some(&wallet.pubkey()),
- );
- let transaction = Transaction::new(&vec![wallet.as_ref()], message, recent_blockhash);
- let signature = rpc
- .send_and_confirm_transaction(&transaction)
- .await
- .unwrap();
- println!("Close position transaction signature: {}", signature);
+ println!("Close position transaction signature: {}", signature);
- let new_lower_price = current_price - (position_upper_price - position_lower_price) / 2.0;
- let new_upper_price = current_price + (position_upper_price - position_lower_price) / 2.0;
- println!("New position price range: [{:.6}, {:.6}]", new_lower_price, new_upper_price);
- println!("Opening new position with adjusted range...");
- let increase_liquidity_param =
- IncreaseLiquidityParam::Liquidity(close_position_instructions.quote.liquidity_delta);
- let open_position_instructions = open_position_instructions(
- &rpc,
- whirlpool_address,
- new_lower_price,
- new_upper_price,
- increase_liquidity_param,
- Some(100),
- None,
- )
- .await
- .unwrap();
- let recent_blockhash = rpc.get_latest_blockhash().await.unwrap();
- let message = Message::new(
- &open_position_instructions.instructions,
- Some(&wallet.pubkey()),
- );
- let mut signers: Vec<&dyn Signer> = vec![wallet.as_ref()];
- signers.extend(
- open_position_instructions
- .additional_signers
- .iter()
- .map(|kp| kp as &dyn Signer),
- );
- let transaction = Transaction::new(&signers, message, recent_blockhash);
- let signature = rpc
- .send_and_confirm_transaction(&transaction)
+ let new_lower_price = current_price - (position_upper_price - position_lower_price) / 2.0;
+ let new_upper_price = current_price + (position_upper_price - position_lower_price) / 2.0;
+ println!("New position price range: [{:.6}, {:.6}]", new_lower_price, new_upper_price);
+ println!("Opening new position with adjusted range...");
+ let increase_liquidity_param =
+ IncreaseLiquidityParam::Liquidity(close_position_instructions.quote.liquidity_delta);
+ let open_position_instructions = open_position_instructions(
+ &rpc,
+ whirlpool_address,
+ new_lower_price,
+ new_upper_price,
+ increase_liquidity_param,
+ Some(100),
+ None,
+ )
.await
.unwrap();
- println!("Open position transaction signature: {}", signature);
- println!("New position mint address: {}", open_position_instructions.position_mint);
- } else {
- println!("Current price is within range. No repositioning needed.");
+ let recent_blockhash = rpc.get_latest_blockhash().await.unwrap();
+ let message = Message::new(
+ &open_position_instructions.instructions,
+ Some(&wallet.pubkey()),
+ );
+ let mut signers: Vec<&dyn Signer> = vec![wallet.as_ref()];
+ signers.extend(
+ open_position_instructions
+ .additional_signers
+ .iter()
+ .map(|kp| kp as &dyn Signer),
+ );
+ let transaction = Transaction::new(&signers, message, recent_blockhash);
+ let signature = rpc
+ .send_and_confirm_transaction(&transaction)
+ .await
+ .unwrap();
+ println!("Open position transaction signature: {}", signature);
+ println!("New position mint address: {}", open_position_instructions.position_mint);
+ position_mint_address = open_position_instructions.position_mint;
+ } else {
+ println!("Current price is within range. No repositioning needed.");
+ }
+ sleep(Duration::from_secs(interval)).await;
}
}
From 5515f3e0bb2026213989311ef29a8b65b85abd46 Mon Sep 17 00:00:00 2001
From: calintje
Date: Sat, 30 Nov 2024 05:27:17 +0100
Subject: [PATCH 03/18] Refactor
---
examples/lp-bot/src/cli.rs | 18 +++
examples/lp-bot/src/main.rs | 152 +++---------------------
examples/lp-bot/src/position_manager.rs | 115 ++++++++++++++++++
examples/lp-bot/src/wallet.rs | 9 ++
4 files changed, 157 insertions(+), 137 deletions(-)
create mode 100644 examples/lp-bot/src/cli.rs
create mode 100644 examples/lp-bot/src/position_manager.rs
create mode 100644 examples/lp-bot/src/wallet.rs
diff --git a/examples/lp-bot/src/cli.rs b/examples/lp-bot/src/cli.rs
new file mode 100644
index 00000000..a41f1cec
--- /dev/null
+++ b/examples/lp-bot/src/cli.rs
@@ -0,0 +1,18 @@
+use clap::Parser;
+
+/// CLI arguments structure
+#[derive(Parser, Debug)]
+#[command(author, version, about, long_about = None)]
+pub struct Args {
+ /// Position mint address
+ #[arg(short, long)]
+ pub position_mint_address: String,
+
+ /// Threshold for repositioning (in percentage)
+ #[arg(short, long, default_value_t = 1.0)]
+ pub threshold: f64,
+
+ /// Time interval for checking (in seconds)
+ #[arg(short, long, default_value_t = 60)]
+ pub interval: u64,
+}
diff --git a/examples/lp-bot/src/main.rs b/examples/lp-bot/src/main.rs
index 4750d6ae..74016654 100644
--- a/examples/lp-bot/src/main.rs
+++ b/examples/lp-bot/src/main.rs
@@ -1,152 +1,30 @@
+mod cli;
+mod position_manager;
+mod wallet;
+
+use std::str::FromStr;
+
use clap::Parser;
-use orca_whirlpools::{
- close_position_instructions, open_position_instructions, set_funder,
- set_whirlpools_config_address, IncreaseLiquidityParam, WhirlpoolsConfigInput,
-};
-use orca_whirlpools_client::{get_position_address, Position, Whirlpool};
-use orca_whirlpools_core::{sqrt_price_to_price, tick_index_to_price};
-use solana_client::nonblocking::rpc_client::RpcClient;
-use solana_sdk::message::{self, Message};
-use solana_sdk::program_pack::Pack;
+use cli::Args;
+use position_manager::run_position_manager;
use solana_sdk::pubkey::Pubkey;
-use solana_sdk::transaction::Transaction;
-use solana_sdk::{signature::Keypair, signer::Signer};
-use spl_token_2022::state::Mint;
-use std::str::FromStr;
-use std::{fs, vec};
use tokio::time::{sleep, Duration};
-#[derive(Parser)]
-#[command(author, version, about, long_about = None)]
-struct Args {
- /// Position mint address
- #[arg(short, long)]
- position_mint_address: String,
-
- /// Threshold for repositioning (in percentage)
- #[arg(short, long, default_value_t = 1.0)]
- threshold: f64,
-
- /// Time interval for checking (in seconds)
- #[arg(short, long, default_value_t = 60)]
- interval: u64,
-}
-
#[tokio::main]
async fn main() {
let args = Args::parse();
-
- set_whirlpools_config_address(WhirlpoolsConfigInput::SolanaMainnet).unwrap();
- let rpc = RpcClient::new("https://api.mainnet-beta.solana.com".to_string());
- let wallet = load_wallet();
- set_funder(wallet.pubkey()).unwrap();
+ let wallet = wallet::load_wallet();
let mut position_mint_address = Pubkey::from_str(&args.position_mint_address).unwrap();
- let threshold = args.threshold;
- let interval = args.interval;
- println!("Position Mint Address: {}", position_mint_address);
- println!("Threshold: {:.2}%", threshold);
- println!("Interval: {} seconds", interval);
+ println!("Position Mint Address: {}", args.position_mint_address);
+ println!("Threshold: {:.2}%", args.threshold);
+ println!("Interval: {} seconds", args.interval);
loop {
- println!("Checking position...");
- let (position_address, _) = get_position_address(&position_mint_address).unwrap();
- let position_account = rpc.get_account(&position_address).await.unwrap();
- let position: Position = Position::from_bytes(&position_account.data).unwrap();
-
- let whirlpool_address = position.whirlpool;
- let whirlpool_account = rpc.get_account(&whirlpool_address).await.unwrap();
- let whirlpool = Whirlpool::from_bytes(&whirlpool_account.data).unwrap();
-
- let token_mint_a_address = whirlpool.token_mint_a;
- let token_mint_a_account = rpc.get_account(&token_mint_a_address).await.unwrap();
- let token_mint_a = Mint::unpack(&token_mint_a_account.data).unwrap();
- let decimals_a = token_mint_a.decimals;
-
- let token_mint_b_address = whirlpool.token_mint_b;
- let token_mint_b_account = rpc.get_account(&token_mint_b_address).await.unwrap();
- let token_mint_b = Mint::unpack(&token_mint_b_account.data).unwrap();
- let decimals_b = token_mint_b.decimals;
-
- let current_price = sqrt_price_to_price(whirlpool.sqrt_price, decimals_a, decimals_b);
- let position_lower_price =
- tick_index_to_price(position.tick_lower_index, decimals_a, decimals_b);
- let position_upper_price =
- tick_index_to_price(position.tick_upper_index, decimals_a, decimals_b);
- let position_center_price = (position_lower_price + position_upper_price) / 2.0;
-
- let deviation = ((current_price - position_center_price).abs() / position_center_price) * 100.0;
- println!("Current pool price: {:.6}", current_price);
- println!("Position price range: [{:.6}, {:.6}]", position_lower_price, position_upper_price);
- println!("Position center price: {:.6}", position_center_price);
- println!("Price deviation from center: {:.2}%", deviation);
-
- if deviation >= threshold {
- println!("Deviation exceeds threshold. Closing position...");
- let close_position_instructions =
- close_position_instructions(&rpc, position_mint_address, Some(100), None)
- .await
- .unwrap();
- let recent_blockhash = rpc.get_latest_blockhash().await.unwrap();
- let message = Message::new(
- &close_position_instructions.instructions,
- Some(&wallet.pubkey()),
- );
- let transaction = Transaction::new(&vec![wallet.as_ref()], message, recent_blockhash);
- let signature = rpc
- .send_and_confirm_transaction(&transaction)
- .await
- .unwrap();
- println!("Close position transaction signature: {}", signature);
-
- let new_lower_price = current_price - (position_upper_price - position_lower_price) / 2.0;
- let new_upper_price = current_price + (position_upper_price - position_lower_price) / 2.0;
- println!("New position price range: [{:.6}, {:.6}]", new_lower_price, new_upper_price);
- println!("Opening new position with adjusted range...");
- let increase_liquidity_param =
- IncreaseLiquidityParam::Liquidity(close_position_instructions.quote.liquidity_delta);
- let open_position_instructions = open_position_instructions(
- &rpc,
- whirlpool_address,
- new_lower_price,
- new_upper_price,
- increase_liquidity_param,
- Some(100),
- None,
- )
- .await
- .unwrap();
- let recent_blockhash = rpc.get_latest_blockhash().await.unwrap();
- let message = Message::new(
- &open_position_instructions.instructions,
- Some(&wallet.pubkey()),
- );
- let mut signers: Vec<&dyn Signer> = vec![wallet.as_ref()];
- signers.extend(
- open_position_instructions
- .additional_signers
- .iter()
- .map(|kp| kp as &dyn Signer),
- );
- let transaction = Transaction::new(&signers, message, recent_blockhash);
- let signature = rpc
- .send_and_confirm_transaction(&transaction)
- .await
- .unwrap();
- println!("Open position transaction signature: {}", signature);
- println!("New position mint address: {}", open_position_instructions.position_mint);
- position_mint_address = open_position_instructions.position_mint;
- } else {
- println!("Current price is within range. No repositioning needed.");
+ if let Err(err) = run_position_manager(&args, &wallet, &mut position_mint_address).await {
+ eprintln!("Error: {}", err);
}
- sleep(Duration::from_secs(interval)).await;
+ sleep(Duration::from_secs(args.interval)).await;
}
}
-
-fn load_wallet() -> Box {
- let wallet_string = fs::read_to_string("wallet.json").unwrap();
- let keypair_bytes: Vec = serde_json::from_str(&wallet_string).unwrap();
- let wallet = Keypair::from_bytes(&keypair_bytes).unwrap();
- Box::new(wallet)
-}
diff --git a/examples/lp-bot/src/position_manager.rs b/examples/lp-bot/src/position_manager.rs
new file mode 100644
index 00000000..868b19ed
--- /dev/null
+++ b/examples/lp-bot/src/position_manager.rs
@@ -0,0 +1,115 @@
+use crate::cli::Args;
+use orca_whirlpools::{
+ close_position_instructions, open_position_instructions, set_funder,
+ set_whirlpools_config_address, IncreaseLiquidityParam, WhirlpoolsConfigInput,
+};
+use orca_whirlpools_client::{get_position_address, Position, Whirlpool};
+use orca_whirlpools_core::{sqrt_price_to_price, tick_index_to_price};
+use solana_client::nonblocking::rpc_client::RpcClient;
+use solana_sdk::{
+ message::Message, program_pack::Pack, pubkey::Pubkey, signer::Signer, transaction::Transaction,
+};
+use spl_token_2022::state::Mint;
+
+pub async fn run_position_manager(
+ args: &Args,
+ wallet: &Box,
+ position_mint_address: &mut Pubkey,
+) -> Result<(), Box> {
+ set_whirlpools_config_address(WhirlpoolsConfigInput::SolanaMainnet).unwrap();
+ let rpc = RpcClient::new("https://api.mainnet-beta.solana.com".to_string());
+ set_funder(wallet.pubkey()).unwrap();
+
+ println!("Checking position...");
+ let (position_address, _) = get_position_address(&position_mint_address).unwrap();
+ let position_account = rpc.get_account(&position_address).await.unwrap();
+ let position: Position = Position::from_bytes(&position_account.data).unwrap();
+
+ let whirlpool_address = position.whirlpool;
+ let whirlpool_account = rpc.get_account(&whirlpool_address).await.unwrap();
+ let whirlpool = Whirlpool::from_bytes(&whirlpool_account.data).unwrap();
+
+ let token_mint_a_account = rpc.get_account(&whirlpool.token_mint_a).await.unwrap();
+ let token_mint_b_account = rpc.get_account(&whirlpool.token_mint_b).await.unwrap();
+ let token_mint_a = Mint::unpack(&token_mint_a_account.data).unwrap();
+ let token_mint_b = Mint::unpack(&token_mint_b_account.data).unwrap();
+
+ let current_price = sqrt_price_to_price(
+ whirlpool.sqrt_price,
+ token_mint_a.decimals,
+ token_mint_b.decimals,
+ );
+ let position_lower_price = tick_index_to_price(
+ position.tick_lower_index,
+ token_mint_a.decimals,
+ token_mint_b.decimals,
+ );
+ let position_upper_price = tick_index_to_price(
+ position.tick_upper_index,
+ token_mint_a.decimals,
+ token_mint_b.decimals,
+ );
+ let position_center_price = (position_lower_price + position_upper_price) / 2.0;
+
+ let deviation = ((current_price - position_center_price).abs() / position_center_price) * 100.0;
+ println!("Current pool price: {:.6}", current_price);
+ println!(
+ "Position price range: [{:.6}, {:.6}]",
+ position_lower_price, position_upper_price
+ );
+ println!("Position center price: {:.6}", position_center_price);
+ println!("Price deviation from center: {:.2}%", deviation);
+
+ if deviation >= args.threshold {
+ println!("Deviation exceeds threshold. Closing position...");
+ let close_position_instructions =
+ close_position_instructions(&rpc, *position_mint_address, Some(100), None).await?;
+ let recent_blockhash = rpc.get_latest_blockhash().await?;
+ let message = Message::new(
+ &close_position_instructions.instructions,
+ Some(&wallet.pubkey()),
+ );
+ let transaction = Transaction::new(&vec![wallet.as_ref()], message, recent_blockhash);
+ let signature = rpc.send_and_confirm_transaction(&transaction).await?;
+ println!("Close position transaction signature: {}", signature);
+
+ let new_lower_price = current_price - (position_upper_price - position_lower_price) / 2.0;
+ let new_upper_price = current_price + (position_upper_price - position_lower_price) / 2.0;
+ println!("Opening new position with adjusted range...");
+ let increase_liquidity_param =
+ IncreaseLiquidityParam::Liquidity(close_position_instructions.quote.liquidity_delta);
+ let open_position_instructions = open_position_instructions(
+ &rpc,
+ whirlpool_address,
+ new_lower_price,
+ new_upper_price,
+ increase_liquidity_param,
+ Some(100),
+ None,
+ )
+ .await?;
+ let recent_blockhash = rpc.get_latest_blockhash().await?;
+ let message = Message::new(
+ &open_position_instructions.instructions,
+ Some(&wallet.pubkey()),
+ );
+ let mut signers: Vec<&dyn Signer> = vec![wallet.as_ref()];
+ signers.extend(
+ open_position_instructions
+ .additional_signers
+ .iter()
+ .map(|kp| kp as &dyn Signer),
+ );
+ let transaction = Transaction::new(&signers, message, recent_blockhash);
+ let signature = rpc.send_and_confirm_transaction(&transaction).await?;
+ println!("Open position transaction signature: {}", signature);
+ println!(
+ "New position mint address: {}",
+ open_position_instructions.position_mint
+ );
+ *position_mint_address = open_position_instructions.position_mint;
+ } else {
+ println!("Current price is within range. No repositioning needed.");
+ }
+ Ok(())
+}
\ No newline at end of file
diff --git a/examples/lp-bot/src/wallet.rs b/examples/lp-bot/src/wallet.rs
new file mode 100644
index 00000000..e43ee8f6
--- /dev/null
+++ b/examples/lp-bot/src/wallet.rs
@@ -0,0 +1,9 @@
+use solana_sdk::{signature::Keypair, signer::Signer};
+use std::fs;
+
+pub fn load_wallet() -> Box {
+ let wallet_string = fs::read_to_string("wallet.json").unwrap();
+ let keypair_bytes: Vec = serde_json::from_str(&wallet_string).unwrap();
+ let wallet = Keypair::from_bytes(&keypair_bytes).unwrap();
+ Box::new(wallet)
+}
From 0953218d167df4d9441c351aa1f11de1ecf07b4d Mon Sep 17 00:00:00 2001
From: calintje
Date: Mon, 2 Dec 2024 04:17:12 +0100
Subject: [PATCH 04/18] Refactor
---
examples/lp-bot/src/main.rs | 1 +
examples/lp-bot/src/position_manager.rs | 59 +++++++++++--------------
examples/lp-bot/src/utils.rs | 50 +++++++++++++++++++++
3 files changed, 77 insertions(+), 33 deletions(-)
create mode 100644 examples/lp-bot/src/utils.rs
diff --git a/examples/lp-bot/src/main.rs b/examples/lp-bot/src/main.rs
index 74016654..d49a8fb9 100644
--- a/examples/lp-bot/src/main.rs
+++ b/examples/lp-bot/src/main.rs
@@ -1,5 +1,6 @@
mod cli;
mod position_manager;
+mod utils;
mod wallet;
use std::str::FromStr;
diff --git a/examples/lp-bot/src/position_manager.rs b/examples/lp-bot/src/position_manager.rs
index 868b19ed..0a899924 100644
--- a/examples/lp-bot/src/position_manager.rs
+++ b/examples/lp-bot/src/position_manager.rs
@@ -1,15 +1,15 @@
-use crate::cli::Args;
+use crate::{
+ cli::Args,
+ utils::{fetch_mint, fetch_position, fetch_whirlpool, send_transaction},
+};
use orca_whirlpools::{
close_position_instructions, open_position_instructions, set_funder,
set_whirlpools_config_address, IncreaseLiquidityParam, WhirlpoolsConfigInput,
};
-use orca_whirlpools_client::{get_position_address, Position, Whirlpool};
+use orca_whirlpools_client::get_position_address;
use orca_whirlpools_core::{sqrt_price_to_price, tick_index_to_price};
use solana_client::nonblocking::rpc_client::RpcClient;
-use solana_sdk::{
- message::Message, program_pack::Pack, pubkey::Pubkey, signer::Signer, transaction::Transaction,
-};
-use spl_token_2022::state::Mint;
+use solana_sdk::{pubkey::Pubkey, signer::Signer};
pub async fn run_position_manager(
args: &Args,
@@ -22,17 +22,13 @@ pub async fn run_position_manager(
println!("Checking position...");
let (position_address, _) = get_position_address(&position_mint_address).unwrap();
- let position_account = rpc.get_account(&position_address).await.unwrap();
- let position: Position = Position::from_bytes(&position_account.data).unwrap();
+ let position = fetch_position(&rpc, &position_address).await?;
let whirlpool_address = position.whirlpool;
- let whirlpool_account = rpc.get_account(&whirlpool_address).await.unwrap();
- let whirlpool = Whirlpool::from_bytes(&whirlpool_account.data).unwrap();
+ let whirlpool = fetch_whirlpool(&rpc, &whirlpool_address).await?;
- let token_mint_a_account = rpc.get_account(&whirlpool.token_mint_a).await.unwrap();
- let token_mint_b_account = rpc.get_account(&whirlpool.token_mint_b).await.unwrap();
- let token_mint_a = Mint::unpack(&token_mint_a_account.data).unwrap();
- let token_mint_b = Mint::unpack(&token_mint_b_account.data).unwrap();
+ let token_mint_a = fetch_mint(&rpc, &whirlpool.token_mint_a).await?;
+ let token_mint_b = fetch_mint(&rpc, &whirlpool.token_mint_b).await?;
let current_price = sqrt_price_to_price(
whirlpool.sqrt_price,
@@ -64,13 +60,13 @@ pub async fn run_position_manager(
println!("Deviation exceeds threshold. Closing position...");
let close_position_instructions =
close_position_instructions(&rpc, *position_mint_address, Some(100), None).await?;
- let recent_blockhash = rpc.get_latest_blockhash().await?;
- let message = Message::new(
- &close_position_instructions.instructions,
- Some(&wallet.pubkey()),
- );
- let transaction = Transaction::new(&vec![wallet.as_ref()], message, recent_blockhash);
- let signature = rpc.send_and_confirm_transaction(&transaction).await?;
+ let signature = send_transaction(
+ &rpc,
+ wallet.as_ref(),
+ close_position_instructions.instructions,
+ vec![],
+ )
+ .await?;
println!("Close position transaction signature: {}", signature);
let new_lower_price = current_price - (position_upper_price - position_lower_price) / 2.0;
@@ -88,20 +84,17 @@ pub async fn run_position_manager(
None,
)
.await?;
- let recent_blockhash = rpc.get_latest_blockhash().await?;
- let message = Message::new(
- &open_position_instructions.instructions,
- Some(&wallet.pubkey()),
- );
- let mut signers: Vec<&dyn Signer> = vec![wallet.as_ref()];
- signers.extend(
+ let signature = send_transaction(
+ &rpc,
+ wallet.as_ref(),
+ open_position_instructions.instructions,
open_position_instructions
.additional_signers
.iter()
- .map(|kp| kp as &dyn Signer),
- );
- let transaction = Transaction::new(&signers, message, recent_blockhash);
- let signature = rpc.send_and_confirm_transaction(&transaction).await?;
+ .map(|kp| kp as &dyn Signer)
+ .collect(),
+ )
+ .await?;
println!("Open position transaction signature: {}", signature);
println!(
"New position mint address: {}",
@@ -112,4 +105,4 @@ pub async fn run_position_manager(
println!("Current price is within range. No repositioning needed.");
}
Ok(())
-}
\ No newline at end of file
+}
diff --git a/examples/lp-bot/src/utils.rs b/examples/lp-bot/src/utils.rs
new file mode 100644
index 00000000..57972a51
--- /dev/null
+++ b/examples/lp-bot/src/utils.rs
@@ -0,0 +1,50 @@
+use orca_whirlpools_client::{Position, Whirlpool};
+use solana_client::nonblocking::rpc_client::RpcClient;
+use solana_sdk::{
+ message::Message, program_pack::Pack, pubkey::Pubkey, signature::Signature, signer::Signer,
+ transaction::Transaction,
+};
+use spl_token_2022::state::Mint;
+
+pub async fn fetch_position(
+ rpc: &RpcClient,
+ position_address: &Pubkey,
+) -> Result> {
+ let position_account = rpc.get_account(position_address).await?;
+ let position = Position::from_bytes(&position_account.data)?;
+ Ok(position)
+}
+
+pub async fn fetch_whirlpool(
+ rpc: &RpcClient,
+ whirlpool_address: &Pubkey,
+) -> Result> {
+ let whirlpool_account = rpc.get_account(whirlpool_address).await?;
+ let whirlpool = Whirlpool::from_bytes(&whirlpool_account.data)?;
+ Ok(whirlpool)
+}
+
+pub async fn fetch_mint(
+ rpc: &RpcClient,
+ mint_address: &Pubkey,
+) -> Result> {
+ let mint_account = rpc.get_account(mint_address).await?;
+ let mint = Mint::unpack(&mint_account.data)?;
+ Ok(mint)
+}
+
+pub async fn send_transaction(
+ rpc: &RpcClient,
+ wallet: &dyn Signer,
+ instructions: Vec,
+ additional_signers: Vec<&dyn Signer>,
+) -> Result> {
+ let recent_blockhash = rpc.get_latest_blockhash().await?;
+ let message = Message::new(&instructions, Some(&wallet.pubkey()));
+ let mut all_signers = vec![wallet];
+ all_signers.extend(additional_signers);
+
+ let transaction = Transaction::new(&all_signers, message, recent_blockhash);
+ let signature = rpc.send_and_confirm_transaction(&transaction).await?;
+ Ok(signature)
+}
From e8f27ec1723ce98310446c1c0bd8d48140a4b16a Mon Sep 17 00:00:00 2001
From: calintje
Date: Wed, 4 Dec 2024 05:19:53 +0100
Subject: [PATCH 05/18] Add README. Add RPC URL as const to main.rs. Add
directory for rust-sdk. Make atomic transaction for close and open position
instructions. Add utility functions for priority fee and client-side retry.
Work in progress: dislapying wallet balances.
---
examples/lp-bot/src/cli.rs | 18 --
examples/lp-bot/src/utils.rs | 50 ----
examples/{ => rust-sdk}/lp-bot/Cargo.toml | 0
examples/rust-sdk/lp-bot/README.md | 106 +++++++
examples/rust-sdk/lp-bot/src/cli.rs | 52 ++++
examples/{ => rust-sdk}/lp-bot/src/main.rs | 18 +-
.../lp-bot/src/position_manager.rs | 48 +--
examples/rust-sdk/lp-bot/src/utils.rs | 277 ++++++++++++++++++
examples/{ => rust-sdk}/lp-bot/src/wallet.rs | 0
9 files changed, 479 insertions(+), 90 deletions(-)
delete mode 100644 examples/lp-bot/src/cli.rs
delete mode 100644 examples/lp-bot/src/utils.rs
rename examples/{ => rust-sdk}/lp-bot/Cargo.toml (100%)
create mode 100644 examples/rust-sdk/lp-bot/README.md
create mode 100644 examples/rust-sdk/lp-bot/src/cli.rs
rename examples/{ => rust-sdk}/lp-bot/src/main.rs (53%)
rename examples/{ => rust-sdk}/lp-bot/src/position_manager.rs (80%)
create mode 100644 examples/rust-sdk/lp-bot/src/utils.rs
rename examples/{ => rust-sdk}/lp-bot/src/wallet.rs (100%)
diff --git a/examples/lp-bot/src/cli.rs b/examples/lp-bot/src/cli.rs
deleted file mode 100644
index a41f1cec..00000000
--- a/examples/lp-bot/src/cli.rs
+++ /dev/null
@@ -1,18 +0,0 @@
-use clap::Parser;
-
-/// CLI arguments structure
-#[derive(Parser, Debug)]
-#[command(author, version, about, long_about = None)]
-pub struct Args {
- /// Position mint address
- #[arg(short, long)]
- pub position_mint_address: String,
-
- /// Threshold for repositioning (in percentage)
- #[arg(short, long, default_value_t = 1.0)]
- pub threshold: f64,
-
- /// Time interval for checking (in seconds)
- #[arg(short, long, default_value_t = 60)]
- pub interval: u64,
-}
diff --git a/examples/lp-bot/src/utils.rs b/examples/lp-bot/src/utils.rs
deleted file mode 100644
index 57972a51..00000000
--- a/examples/lp-bot/src/utils.rs
+++ /dev/null
@@ -1,50 +0,0 @@
-use orca_whirlpools_client::{Position, Whirlpool};
-use solana_client::nonblocking::rpc_client::RpcClient;
-use solana_sdk::{
- message::Message, program_pack::Pack, pubkey::Pubkey, signature::Signature, signer::Signer,
- transaction::Transaction,
-};
-use spl_token_2022::state::Mint;
-
-pub async fn fetch_position(
- rpc: &RpcClient,
- position_address: &Pubkey,
-) -> Result> {
- let position_account = rpc.get_account(position_address).await?;
- let position = Position::from_bytes(&position_account.data)?;
- Ok(position)
-}
-
-pub async fn fetch_whirlpool(
- rpc: &RpcClient,
- whirlpool_address: &Pubkey,
-) -> Result> {
- let whirlpool_account = rpc.get_account(whirlpool_address).await?;
- let whirlpool = Whirlpool::from_bytes(&whirlpool_account.data)?;
- Ok(whirlpool)
-}
-
-pub async fn fetch_mint(
- rpc: &RpcClient,
- mint_address: &Pubkey,
-) -> Result> {
- let mint_account = rpc.get_account(mint_address).await?;
- let mint = Mint::unpack(&mint_account.data)?;
- Ok(mint)
-}
-
-pub async fn send_transaction(
- rpc: &RpcClient,
- wallet: &dyn Signer,
- instructions: Vec,
- additional_signers: Vec<&dyn Signer>,
-) -> Result> {
- let recent_blockhash = rpc.get_latest_blockhash().await?;
- let message = Message::new(&instructions, Some(&wallet.pubkey()));
- let mut all_signers = vec![wallet];
- all_signers.extend(additional_signers);
-
- let transaction = Transaction::new(&all_signers, message, recent_blockhash);
- let signature = rpc.send_and_confirm_transaction(&transaction).await?;
- Ok(signature)
-}
diff --git a/examples/lp-bot/Cargo.toml b/examples/rust-sdk/lp-bot/Cargo.toml
similarity index 100%
rename from examples/lp-bot/Cargo.toml
rename to examples/rust-sdk/lp-bot/Cargo.toml
diff --git a/examples/rust-sdk/lp-bot/README.md b/examples/rust-sdk/lp-bot/README.md
new file mode 100644
index 00000000..893de3c1
--- /dev/null
+++ b/examples/rust-sdk/lp-bot/README.md
@@ -0,0 +1,106 @@
+# LP Bot - position rebalance
+A Rust-based CLI bot for interacting with the Orca Whirlpools program on Solana. This bot monitors and rebalances a liquidity position by closing and reopening positions when price deviations exceed a user-defined threshold.
+
+---
+
+## Features
+- **Automated Position Monitoring**: Monitors price deviation of a liquidity position on Orca Whirlpool by calculating the center of the position's price range and comparing it to the current pool price. If the deviation exceeds the specified threshold (in percentage), the bot initiates rebalancing.
+- **Automated Rebalancing**: Closes and reopens liquidity positions by centering the new position around the current pool price, maintaining the same width (distance between the lower and upper price bounds) as the initial position.
+- **Customizable Priority Fees**: Integrates compute budget priority fees to enhance transaction speed and landing, with options ranging from `none` to `turbo` for different levels of prioritization.
+
+---
+
+## Prerequisites
+1. **Solana Wallet**:
+ - Place a `wallet.json` file in the working directory with the keypair that owns the positions.
+ - Ensure the wallet has sufficient funds for transactions.
+2. **Existing Position**:
+ - You must have an active position on Orca Whirlpools. You can open a position using our SDKs or through our UI at https://www.orca.so/pools.
+3. **Rust**:
+ - Install Rust using [rustup](https://rustup.rs/).
+
+---
+
+## Installation
+1. Clone this repository:
+ ```bash
+ git clone https://github.com/orca-so/whirlpools.git
+ cd examples/rust-sdk/lp-bot
+ ```
+2. Build the bot:
+ ```bash
+ cargo build --release
+ ```
+3. The executable will be located in target/release/lp-bot
+
+---
+
+## RPC Configuration
+The bot connects to Solana Mainnet Beta by default using:
+
+```rust
+const RPC_URL: &str = "https://api.mainnet-beta.solana.com";
+```
+
+To modify this, update the RPC_URL constant in main.rs.
+
+---
+
+## Usage
+Run the bot with the following arguments
+```bash
+./target/release/lp-bot \
+ --position-mint-address \
+ --threshold \
+ --interval \
+ --priority-fee-tier
+```
+
+### Arguments
+- `--position-mint-address` (required): The mint address of the position to monitor and rebalance.
+- `--threshold` (optional): The percentage deviation from the center price at which rebalancing is triggered. Default: 1.0.
+- `--interval` (optional): The time interval (in seconds) between checks. Default: 60.
+- `--priority-fee-tier` (optional): The priority fee tier for transaction processing. Options:
+ - `none`: No priority fee.
+ - `low`: Lower 25th quartile prioritization fee.
+ - `medium`: Median prioritization fee (default).
+ - `high`: Upper 75th quartile prioritization fee.
+ - `turbo`: Upper 95th quartile prioritization fee.
+
+### Example Usage
+Monitor and rebalance with default settings:
+```bash
+./target/release/lp-bot \
+ --position-mint-address 5m1izNWC3ioBaKm63e3gSNFeZ44o13ncre5QknTXBJUS
+```
+
+Monitor with custom threshold and interval:
+```bash
+./target/release/lp-bot \
+ --position-mint-address 5m1izNWC3ioBaKm63e3gSNFeZ44o13ncre5QknTXBJUS \
+ --threshold 0.5 \
+ --interval 30
+```
+
+Monitor with turbo priority fees:
+```bash
+./target/release/lp-bot \
+ --position-mint-address 5m1izNWC3ioBaKm63e3gSNFeZ44o13ncre5QknTXBJUS \
+ --priority-fee-tier turbo
+```
+
+---
+
+## Directory Structure
+
+```bash
+examples/
+├── rust-sdk/
+ └── lp-bot/
+ └── src/
+ ├── main.rs # Entry point
+ ├── cli.rs # CLI argument parsing
+ ├── wallet.rs # Wallet management
+ ├── position_manager.rs # Position monitoring and rebalancing
+ ├── solana_utils.rs # RPC utilities
+```
\ No newline at end of file
diff --git a/examples/rust-sdk/lp-bot/src/cli.rs b/examples/rust-sdk/lp-bot/src/cli.rs
new file mode 100644
index 00000000..2cd9ca11
--- /dev/null
+++ b/examples/rust-sdk/lp-bot/src/cli.rs
@@ -0,0 +1,52 @@
+use clap::Parser;
+
+use crate::utils::PriorityFeeTier;
+
+#[derive(Parser, Debug)]
+#[command(author, version, about, long_about = None)]
+pub struct Args {
+ #[arg(
+ short = 'p',
+ long,
+ help = "The position mint address to monitor and rebalance."
+ )]
+ pub position_mint_address: String,
+
+ #[arg(
+ short = 't',
+ long,
+ default_value_t = 1.0,
+ help = "Threshold for repositioning in percentage.\n"
+ )]
+ pub threshold: f64,
+
+ #[arg(
+ short = 'i',
+ long,
+ default_value_t = 60,
+ help = "Time interval for checking in seconds.\n"
+ )]
+ pub interval: u64,
+
+ #[arg(
+ short = 'f',
+ long,
+ value_enum,
+ default_value_t = PriorityFeeTier::Medium,
+ help = "Priority fee tier for transaction processing based on recently paid priority fees. Options:\n \
+ - `none`: No priority fee\n \
+ - `low`: Lower 25th quartile prioritization fee\n \
+ - `medium`: Median prioritization fee\n \
+ - `high`: Upper 75th quartile prioritization fee\n \
+ - `turbo`: Upper 95th quartile prioritization fee\n"
+ )]
+ pub priority_fee_tier: PriorityFeeTier,
+
+ #[arg(
+ short = 'm',
+ long,
+ default_value_t = 10_000_000,
+ help = "Maximum total priority fee in lamports.\n"
+ )]
+ pub max_priority_fee_lamports: u64,
+}
diff --git a/examples/lp-bot/src/main.rs b/examples/rust-sdk/lp-bot/src/main.rs
similarity index 53%
rename from examples/lp-bot/src/main.rs
rename to examples/rust-sdk/lp-bot/src/main.rs
index d49a8fb9..571d2f2c 100644
--- a/examples/lp-bot/src/main.rs
+++ b/examples/rust-sdk/lp-bot/src/main.rs
@@ -11,6 +11,9 @@ use position_manager::run_position_manager;
use solana_sdk::pubkey::Pubkey;
use tokio::time::{sleep, Duration};
+pub const RPC_URL: &str =
+ "https://mainnet.helius-rpc.com/?api-key=e1bbe936-f564-4d9a-ae4e-a69e6f99e9b1";
+
#[tokio::main]
async fn main() {
let args = Args::parse();
@@ -18,9 +21,18 @@ async fn main() {
let mut position_mint_address = Pubkey::from_str(&args.position_mint_address).unwrap();
- println!("Position Mint Address: {}", args.position_mint_address);
- println!("Threshold: {:.2}%", args.threshold);
- println!("Interval: {} seconds", args.interval);
+ println!(
+ "\n\
+ ====================\n\
+ 🌀 Whirlpool LP BOT \n\
+ ====================\n"
+ );
+ println!("Configuration:");
+ println!(
+ " Position Mint Address: {}\n Threshold: {:.2}%\n Interval: {} seconds\n Priority Fee Tier: {:?}\n",
+ args.position_mint_address, args.threshold, args.interval, args.priority_fee_tier
+ );
+ println!("-------------------------------------\n");
loop {
if let Err(err) = run_position_manager(&args, &wallet, &mut position_mint_address).await {
diff --git a/examples/lp-bot/src/position_manager.rs b/examples/rust-sdk/lp-bot/src/position_manager.rs
similarity index 80%
rename from examples/lp-bot/src/position_manager.rs
rename to examples/rust-sdk/lp-bot/src/position_manager.rs
index 0a899924..7f1470b8 100644
--- a/examples/lp-bot/src/position_manager.rs
+++ b/examples/rust-sdk/lp-bot/src/position_manager.rs
@@ -1,6 +1,7 @@
use crate::{
cli::Args,
utils::{fetch_mint, fetch_position, fetch_whirlpool, send_transaction},
+ RPC_URL,
};
use orca_whirlpools::{
close_position_instructions, open_position_instructions, set_funder,
@@ -17,7 +18,7 @@ pub async fn run_position_manager(
position_mint_address: &mut Pubkey,
) -> Result<(), Box> {
set_whirlpools_config_address(WhirlpoolsConfigInput::SolanaMainnet).unwrap();
- let rpc = RpcClient::new("https://api.mainnet-beta.solana.com".to_string());
+ let rpc = RpcClient::new(RPC_URL.to_string());
set_funder(wallet.pubkey()).unwrap();
println!("Checking position...");
@@ -27,6 +28,10 @@ pub async fn run_position_manager(
let whirlpool_address = position.whirlpool;
let whirlpool = fetch_whirlpool(&rpc, &whirlpool_address).await?;
+ // let token_a_pubkey = whirlpool.token_mint_a;
+ // let token_b_pubkey = whirlpool.token_mint_b;
+ // display_wallet_balances(&rpc, &token_a_pubkey, &token_b_pubkey).await?;
+
let token_mint_a = fetch_mint(&rpc, &whirlpool.token_mint_a).await?;
let token_mint_b = fetch_mint(&rpc, &whirlpool.token_mint_b).await?;
@@ -57,21 +62,14 @@ pub async fn run_position_manager(
println!("Price deviation from center: {:.2}%", deviation);
if deviation >= args.threshold {
- println!("Deviation exceeds threshold. Closing position...");
+ println!("Deviation exceeds threshold. Rebalancing position...");
+
let close_position_instructions =
close_position_instructions(&rpc, *position_mint_address, Some(100), None).await?;
- let signature = send_transaction(
- &rpc,
- wallet.as_ref(),
- close_position_instructions.instructions,
- vec![],
- )
- .await?;
- println!("Close position transaction signature: {}", signature);
let new_lower_price = current_price - (position_upper_price - position_lower_price) / 2.0;
let new_upper_price = current_price + (position_upper_price - position_lower_price) / 2.0;
- println!("Opening new position with adjusted range...");
+
let increase_liquidity_param =
IncreaseLiquidityParam::Liquidity(close_position_instructions.quote.liquidity_delta);
let open_position_instructions = open_position_instructions(
@@ -84,23 +82,35 @@ pub async fn run_position_manager(
None,
)
.await?;
- let signature = send_transaction(
- &rpc,
- wallet.as_ref(),
- open_position_instructions.instructions,
+
+ let mut all_instructions = vec![];
+ all_instructions.extend(close_position_instructions.instructions);
+ all_instructions.extend(open_position_instructions.instructions);
+
+ let mut signers: Vec<&dyn Signer> = vec![wallet.as_ref()];
+ signers.extend(
open_position_instructions
.additional_signers
.iter()
- .map(|kp| kp as &dyn Signer)
- .collect(),
+ .map(|kp| kp as &dyn Signer),
+ );
+
+ let signature = send_transaction(
+ &rpc,
+ wallet.as_ref(),
+ all_instructions,
+ signers,
+ args.priority_fee_tier,
+ args.max_priority_fee_lamports,
)
.await?;
- println!("Open position transaction signature: {}", signature);
+ println!("Rebalancing transaction signature: {}", signature);
+
+ *position_mint_address = open_position_instructions.position_mint;
println!(
"New position mint address: {}",
open_position_instructions.position_mint
);
- *position_mint_address = open_position_instructions.position_mint;
} else {
println!("Current price is within range. No repositioning needed.");
}
diff --git a/examples/rust-sdk/lp-bot/src/utils.rs b/examples/rust-sdk/lp-bot/src/utils.rs
new file mode 100644
index 00000000..b75a71f9
--- /dev/null
+++ b/examples/rust-sdk/lp-bot/src/utils.rs
@@ -0,0 +1,277 @@
+use clap::ValueEnum;
+use orca_whirlpools_client::{Position, Whirlpool};
+use solana_client::nonblocking::rpc_client::RpcClient;
+use solana_sdk::compute_budget::ComputeBudgetInstruction;
+use solana_sdk::{
+ message::Message, program_pack::Pack, pubkey::Pubkey, signature::Signature, signer::Signer,
+ transaction::Transaction,
+};
+use spl_token_2022::state::Mint;
+use std::future::Future;
+use tokio::time::sleep;
+use tokio::time::Duration;
+
+const MAX_RETRIES: usize = 3;
+const INITIAL_RETRY_DELAY: Duration = Duration::from_millis(100);
+
+// pub async fn display_wallet_balances(
+// rpc: &RpcClient,
+// token_a_address: &Pubkey,
+// token_b_address: &Pubkey,
+// ) -> Result<(), Box> {
+// let token_a_balance = fetch_token_balance(rpc, token_a_address).await?;
+// let token_b_balance = fetch_token_balance(rpc, token_b_address).await?;
+
+// println!(
+// "Wallet Balances: \n\
+// - Token A ({:?}): {} \n\
+// - Token B ({:?}): {}",
+// token_a_address, token_a_balance, token_b_address, token_b_balance
+// );
+
+// Ok(())
+// }
+
+// async fn fetch_token_balance(
+// rpc: &RpcClient,
+// token_address: &Pubkey,
+// ) -> Result> {
+// retry_async(
+// || async {
+// let balance = rpc.get_token_account_balance(token_address).await?;
+// Ok(balance.ui_amount_string)
+// },
+// MAX_RETRIES,
+// INITIAL_RETRY_DELAY,
+// "fetch token balance",
+// )
+// .await
+// }
+
+pub async fn fetch_position(
+ rpc: &RpcClient,
+ position_address: &Pubkey,
+) -> Result> {
+ retry_async(
+ || async {
+ let position_account = rpc.get_account(position_address).await?;
+ let position = Position::from_bytes(&position_account.data)?;
+ Ok(position)
+ },
+ MAX_RETRIES,
+ INITIAL_RETRY_DELAY,
+ "fetch position",
+ )
+ .await
+}
+
+pub async fn fetch_whirlpool(
+ rpc: &RpcClient,
+ whirlpool_address: &Pubkey,
+) -> Result> {
+ retry_async(
+ || async {
+ let whirlpool_account = rpc.get_account(whirlpool_address).await?;
+ let whirlpool = Whirlpool::from_bytes(&whirlpool_account.data)?;
+ Ok(whirlpool)
+ },
+ MAX_RETRIES,
+ INITIAL_RETRY_DELAY,
+ "fetch whirlpool",
+ )
+ .await
+}
+
+pub async fn fetch_mint(
+ rpc: &RpcClient,
+ mint_address: &Pubkey,
+) -> Result> {
+ retry_async(
+ || async {
+ let mint_account = rpc.get_account(mint_address).await?;
+ let mint = Mint::unpack(&mint_account.data)?;
+ Ok(mint)
+ },
+ MAX_RETRIES,
+ INITIAL_RETRY_DELAY,
+ "fetch mint",
+ )
+ .await
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
+pub enum PriorityFeeTier {
+ None,
+ Low,
+ Medium,
+ High,
+ Turbo,
+}
+
+pub async fn send_transaction(
+ rpc: &RpcClient,
+ wallet: &dyn Signer,
+ instructions: Vec,
+ additional_signers: Vec<&dyn Signer>,
+ tier: PriorityFeeTier,
+ max_priority_fee: u64,
+) -> Result> {
+ retry_async(
+ || async {
+ let mut all_instructions = vec![];
+
+ if let Some(priority_fee_instruction) = get_priority_fee_instruction(
+ rpc,
+ &instructions,
+ wallet,
+ &additional_signers,
+ tier,
+ max_priority_fee,
+ )
+ .await?
+ {
+ all_instructions.push(priority_fee_instruction);
+ }
+
+ all_instructions.extend(instructions.clone());
+
+ let recent_blockhash = rpc.get_latest_blockhash().await?;
+ let message = Message::new(&all_instructions, Some(&wallet.pubkey()));
+ let mut all_signers = vec![wallet];
+ all_signers.extend(additional_signers.clone());
+
+ let transaction = Transaction::new(&all_signers, message, recent_blockhash);
+ let signature = rpc.send_and_confirm_transaction(&transaction).await?;
+ Ok(signature)
+ },
+ MAX_RETRIES,
+ INITIAL_RETRY_DELAY,
+ "send transaction",
+ )
+ .await
+}
+
+async fn get_priority_fee_instruction(
+ rpc: &RpcClient,
+ instructions: &[solana_sdk::instruction::Instruction],
+ wallet: &dyn Signer,
+ additional_signers: &[&dyn Signer],
+ tier: PriorityFeeTier,
+ max_priority_fee_lamports: u64,
+) -> Result
=20.0.0
"@types/node": ^18.0.0 || >=20.0.0
less: "*"
lightningcss: ^1.21.0
@@ -18505,7 +17775,6 @@ __metadata:
stylus: "*"
sugarss: "*"
terser: ^5.4.0
- terser: ^5.4.0
dependenciesMeta:
fsevents:
optional: true
@@ -18529,24 +17798,13 @@ __metadata:
bin:
vite: bin/vite.js
checksum: 10c0/d536bb7af57dd0eca2a808f95f5ff1d7b7ffb8d86e17c6893087680a0448bd0d15e07475270c8a6de65cb5115592d037130a1dd979dc76bcef8c1dda202a1874
- checksum: 10c0/d536bb7af57dd0eca2a808f95f5ff1d7b7ffb8d86e17c6893087680a0448bd0d15e07475270c8a6de65cb5115592d037130a1dd979dc76bcef8c1dda202a1874
languageName: node
linkType: hard
"vitest@npm:^2.1.8":
version: 2.1.8
resolution: "vitest@npm:2.1.8"
-"vitest@npm:^2.1.6":
- version: 2.1.8
- resolution: "vitest@npm:2.1.8"
dependencies:
- "@vitest/expect": "npm:2.1.8"
- "@vitest/mocker": "npm:2.1.8"
- "@vitest/pretty-format": "npm:^2.1.8"
- "@vitest/runner": "npm:2.1.8"
- "@vitest/snapshot": "npm:2.1.8"
- "@vitest/spy": "npm:2.1.8"
- "@vitest/utils": "npm:2.1.8"
"@vitest/expect": "npm:2.1.8"
"@vitest/mocker": "npm:2.1.8"
"@vitest/pretty-format": "npm:^2.1.8"
@@ -18566,17 +17824,12 @@ __metadata:
tinyrainbow: "npm:^1.2.0"
vite: "npm:^5.0.0"
vite-node: "npm:2.1.8"
- vite: "npm:^5.0.0"
- vite-node: "npm:2.1.8"
why-is-node-running: "npm:^2.3.0"
peerDependencies:
"@edge-runtime/vm": "*"
"@types/node": ^18.0.0 || >=20.0.0
"@vitest/browser": 2.1.8
"@vitest/ui": 2.1.8
- "@types/node": ^18.0.0 || >=20.0.0
- "@vitest/browser": 2.1.8
- "@vitest/ui": 2.1.8
happy-dom: "*"
jsdom: "*"
peerDependenciesMeta:
@@ -18595,7 +17848,6 @@ __metadata:
bin:
vitest: vitest.mjs
checksum: 10c0/e70631bad5662d6c60c5cf836a4baf58b890db6654fef1f608fe6a86aa49a2b9f078aac74b719d4d3c87c5c781968cc73590a7935277b48f3d8b6fb9c5b4d276
- checksum: 10c0/e70631bad5662d6c60c5cf836a4baf58b890db6654fef1f608fe6a86aa49a2b9f078aac74b719d4d3c87c5c781968cc73590a7935277b48f3d8b6fb9c5b4d276
languageName: node
linkType: hard
@@ -18796,8 +18048,8 @@ __metadata:
linkType: hard
"webpack@npm:^5.88.1, webpack@npm:^5.95.0":
- version: 5.97.0
- resolution: "webpack@npm:5.97.0"
+ version: 5.97.1
+ resolution: "webpack@npm:5.97.1"
dependencies:
"@types/eslint-scope": "npm:^3.7.7"
"@types/estree": "npm:^1.0.6"
@@ -18827,7 +18079,7 @@ __metadata:
optional: true
bin:
webpack: bin/webpack.js
- checksum: 10c0/a8714d42defbf52382b61c157f68e161a16d0edf228d8d9abaa7a165f3ee0ac7386a08d28d4dcf8d6740ea5bda0c4d4abfeeb838df029e636c1c28bb2454ac56
+ checksum: 10c0/a12d3dc882ca582075f2c4bd88840be8307427245c90a8a0e0b372d73560df13fcf25a61625c9e7edc964981d16b5a8323640562eb48347cf9dd2f8bd1b39d35
languageName: node
linkType: hard
From ea55dbbc700a9164b3ed2957805e75d58ae1cd37 Mon Sep 17 00:00:00 2001
From: calintje
Date: Sat, 7 Dec 2024 04:43:47 +0100
Subject: [PATCH 16/18] Format
---
.../src/position_manager.rs | 16 ++++++++--------
.../whirlpool_repositioning_bot/src/utils.rs | 2 +-
examples/ts-sdk/next/next.config.js | 12 +++++++-----
3 files changed, 16 insertions(+), 14 deletions(-)
diff --git a/examples/rust-sdk/whirlpool_repositioning_bot/src/position_manager.rs b/examples/rust-sdk/whirlpool_repositioning_bot/src/position_manager.rs
index f0fd72d9..abec665c 100644
--- a/examples/rust-sdk/whirlpool_repositioning_bot/src/position_manager.rs
+++ b/examples/rust-sdk/whirlpool_repositioning_bot/src/position_manager.rs
@@ -26,7 +26,7 @@ pub async fn run_position_manager(
println!("Checking position.");
let whirlpool_address = position.whirlpool;
- let whirlpool = fetch_whirlpool(&rpc, &whirlpool_address)
+ let whirlpool = fetch_whirlpool(rpc, &whirlpool_address)
.await
.map_err(|_| "Failed to fetch Whirlpool data.")?;
@@ -75,7 +75,7 @@ pub async fn run_position_manager(
);
let close_position_instructions = close_position_instructions(
- &rpc,
+ rpc,
position.position_mint,
Some(args.slippage_tolerance_bps),
None,
@@ -89,7 +89,7 @@ pub async fn run_position_manager(
let increase_liquidity_param =
IncreaseLiquidityParam::Liquidity(close_position_instructions.quote.liquidity_delta);
let open_position_instructions = open_position_instructions(
- &rpc,
+ rpc,
whirlpool_address,
new_lower_price,
new_upper_price,
@@ -119,7 +119,7 @@ pub async fn run_position_manager(
);
let signature = send_transaction(
- &rpc,
+ rpc,
wallet.as_ref(),
&whirlpool_address,
all_instructions,
@@ -135,12 +135,12 @@ pub async fn run_position_manager(
println!("New position mint address: {}", position_mint_address);
let (position_address, _) = get_position_address(&position_mint_address)
.map_err(|_| "Failed to derive new position address.")?;
- *position = fetch_position(&rpc, &position_address)
+ *position = fetch_position(rpc, &position_address)
.await
.map_err(|_| "Failed to fetch new position data.")?;
display_wallet_balances(
- &rpc,
+ rpc,
&wallet.pubkey(),
&whirlpool.token_mint_a,
&whirlpool.token_mint_b,
@@ -149,8 +149,8 @@ pub async fn run_position_manager(
.map_err(|_| "Failed to display wallet balances.")?;
display_position_balances(
- &rpc,
- &position,
+ rpc,
+ position,
&whirlpool.token_mint_a,
&whirlpool.token_mint_b,
token_mint_a.decimals,
diff --git a/examples/rust-sdk/whirlpool_repositioning_bot/src/utils.rs b/examples/rust-sdk/whirlpool_repositioning_bot/src/utils.rs
index e2df46bd..7b4b2f65 100644
--- a/examples/rust-sdk/whirlpool_repositioning_bot/src/utils.rs
+++ b/examples/rust-sdk/whirlpool_repositioning_bot/src/utils.rs
@@ -26,7 +26,7 @@ pub async fn display_position_balances(
slippage_tolerance_bps: u16,
) -> Result<(), Box> {
let close_position_instructions = close_position_instructions(
- &rpc,
+ rpc,
position.position_mint,
Some(slippage_tolerance_bps),
None,
diff --git a/examples/ts-sdk/next/next.config.js b/examples/ts-sdk/next/next.config.js
index 29d30ebe..817af15c 100644
--- a/examples/ts-sdk/next/next.config.js
+++ b/examples/ts-sdk/next/next.config.js
@@ -11,11 +11,13 @@ const nextConfig = {
// (local dependencies are symlinked and next doesn't like that)
config.plugins.push(
new CopyWebpackPlugin({
- patterns: [{
- from: "../../../ts-sdk/core/dist/nodejs/orca_whirlpools_core_js_bindings_bg.wasm",
- to: "./server/app"
- }],
- })
+ patterns: [
+ {
+ from: "../../../ts-sdk/core/dist/nodejs/orca_whirlpools_core_js_bindings_bg.wasm",
+ to: "./server/app",
+ },
+ ],
+ }),
);
// The following supresses a warning about using top-level-await and is optional
From 294a041d2263b0405700fb550c923ae8c9b04f24 Mon Sep 17 00:00:00 2001
From: calintje
Date: Sat, 7 Dec 2024 15:31:50 +0100
Subject: [PATCH 17/18] Remove test script from package.json
---
examples/rust-sdk/whirlpool_repositioning_bot/package.json | 1 -
1 file changed, 1 deletion(-)
diff --git a/examples/rust-sdk/whirlpool_repositioning_bot/package.json b/examples/rust-sdk/whirlpool_repositioning_bot/package.json
index de7b1d45..2a6dbf9c 100644
--- a/examples/rust-sdk/whirlpool_repositioning_bot/package.json
+++ b/examples/rust-sdk/whirlpool_repositioning_bot/package.json
@@ -3,7 +3,6 @@
"version": "0.0.1",
"scripts": {
"build": "cargo build",
- "test": "cargo test --lib",
"format": "cargo clippy --fix --allow-dirty --allow-staged && cargo fmt",
"lint": "cargo clippy",
"clean": "cargo clean"
From bf1504be5b6a0561714e3406e8de9a79b9577168 Mon Sep 17 00:00:00 2001
From: calintje
Date: Sat, 7 Dec 2024 21:03:45 +0100
Subject: [PATCH 18/18] Update lint script
---
examples/rust-sdk/whirlpool_repositioning_bot/package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/examples/rust-sdk/whirlpool_repositioning_bot/package.json b/examples/rust-sdk/whirlpool_repositioning_bot/package.json
index 2a6dbf9c..7c8f2a23 100644
--- a/examples/rust-sdk/whirlpool_repositioning_bot/package.json
+++ b/examples/rust-sdk/whirlpool_repositioning_bot/package.json
@@ -4,7 +4,7 @@
"scripts": {
"build": "cargo build",
"format": "cargo clippy --fix --allow-dirty --allow-staged && cargo fmt",
- "lint": "cargo clippy",
+ "lint": "cargo clippy && cargo fmt --check",
"clean": "cargo clean"
},
"devDependencies": {