Client: Add jupiter_swap (#139)
This commit is contained in:
parent
331bb7ebf0
commit
681c69e3a5
|
@ -1260,17 +1260,24 @@ dependencies = [
|
|||
"anchor-lang 0.25.0",
|
||||
"anchor-spl 0.25.0",
|
||||
"anyhow",
|
||||
"base64 0.13.0",
|
||||
"bincode",
|
||||
"fixed",
|
||||
"fixed-macro",
|
||||
"itertools 0.10.3",
|
||||
"log 0.4.17",
|
||||
"mango-v4",
|
||||
"pyth-sdk-solana",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serum_dex 0.4.0 (git+https://github.com/blockworks-foundation/serum-dex.git)",
|
||||
"shellexpand",
|
||||
"solana-account-decoder",
|
||||
"solana-client",
|
||||
"solana-sdk",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -4581,9 +4588,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.11.10"
|
||||
version = "0.11.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb"
|
||||
checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"base64 0.13.0",
|
||||
|
@ -4607,14 +4614,15 @@ dependencies = [
|
|||
"percent-encoding 2.1.0",
|
||||
"pin-project-lite",
|
||||
"rustls",
|
||||
"rustls-pemfile 0.3.0",
|
||||
"rustls-pemfile 1.0.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tokio-util 0.6.10",
|
||||
"tokio-util 0.7.2",
|
||||
"tower-service",
|
||||
"url 2.2.2",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
|
@ -4762,15 +4770,6 @@ dependencies = [
|
|||
"base64 0.13.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ee86d63972a7c661d1536fefe8c3c8407321c3df668891286de28abcd087360"
|
||||
dependencies = [
|
||||
"base64 0.13.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "1.0.0"
|
||||
|
@ -4947,9 +4946,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.137"
|
||||
version = "1.0.141"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
|
||||
checksum = "7af873f2c95b99fcb0bd0fe622a43e29514658873c8ceba88c4cb88833a22500"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
@ -4965,9 +4964,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.137"
|
||||
version = "1.0.141"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be"
|
||||
checksum = "75743a150d003dd863b51dc809bcad0d73f2102c53632f1e954e738192a3413f"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.39",
|
||||
"quote 1.0.18",
|
||||
|
@ -4987,9 +4986,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.81"
|
||||
version = "1.0.82"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
|
||||
checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
|
|
2
anchor
2
anchor
|
@ -1 +1 @@
|
|||
Subproject commit 442c00634e847af84672727dc00b45a2c8d0a956
|
||||
Subproject commit 2058b6461cb0de5af90b04eb8fae4225a368251e
|
145
cli/src/main.rs
145
cli/src/main.rs
|
@ -1,6 +1,8 @@
|
|||
use clap::{Parser, Subcommand};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use client::MangoClient;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
#[clap()]
|
||||
|
@ -9,8 +11,83 @@ struct Cli {
|
|||
command: Command,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug, Clone)]
|
||||
struct Rpc {
|
||||
#[clap(short, long, default_value = "m")]
|
||||
url: String,
|
||||
|
||||
#[clap(short, long, default_value = "")]
|
||||
fee_payer: String,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug, Clone)]
|
||||
struct CreateAccount {
|
||||
#[clap(short, long)]
|
||||
group: String,
|
||||
|
||||
/// also pays for everything
|
||||
#[clap(short, long)]
|
||||
owner: String,
|
||||
|
||||
#[clap(short, long)]
|
||||
account_num: Option<u32>,
|
||||
|
||||
#[clap(short, long, default_value = "")]
|
||||
name: String,
|
||||
|
||||
#[clap(flatten)]
|
||||
rpc: Rpc,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug, Clone)]
|
||||
struct Deposit {
|
||||
#[clap(long)]
|
||||
account: String,
|
||||
|
||||
/// also pays for everything
|
||||
#[clap(short, long)]
|
||||
owner: String,
|
||||
|
||||
#[clap(short, long)]
|
||||
mint: String,
|
||||
|
||||
#[clap(short, long)]
|
||||
amount: u64,
|
||||
|
||||
#[clap(flatten)]
|
||||
rpc: Rpc,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug, Clone)]
|
||||
struct JupiterSwap {
|
||||
#[clap(long)]
|
||||
account: String,
|
||||
|
||||
/// also pays for everything
|
||||
#[clap(short, long)]
|
||||
owner: String,
|
||||
|
||||
#[clap(long)]
|
||||
input_mint: String,
|
||||
|
||||
#[clap(long)]
|
||||
output_mint: String,
|
||||
|
||||
#[clap(short, long)]
|
||||
amount: u64,
|
||||
|
||||
#[clap(short, long)]
|
||||
slippage: f64,
|
||||
|
||||
#[clap(flatten)]
|
||||
rpc: Rpc,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
enum Command {
|
||||
CreateAccount(CreateAccount),
|
||||
Deposit(Deposit),
|
||||
JupiterSwap(JupiterSwap),
|
||||
GroupAddress {
|
||||
#[clap(short, long)]
|
||||
creator: String,
|
||||
|
@ -26,9 +103,22 @@ enum Command {
|
|||
owner: String,
|
||||
|
||||
#[clap(short, long, default_value = "0")]
|
||||
num: u8,
|
||||
num: u32,
|
||||
},
|
||||
}
|
||||
|
||||
impl Rpc {
|
||||
fn client(&self, override_fee_payer: Option<&str>) -> anyhow::Result<client::Client> {
|
||||
let fee_payer = client::keypair_from_cli(override_fee_payer.unwrap_or(&self.fee_payer));
|
||||
Ok(client::Client {
|
||||
cluster: anchor_client::Cluster::from_str(&self.url)?,
|
||||
commitment: solana_sdk::commitment_config::CommitmentConfig::confirmed(),
|
||||
fee_payer: Arc::new(fee_payer),
|
||||
timeout: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), anyhow::Error> {
|
||||
env_logger::init_from_env(
|
||||
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
|
||||
|
@ -38,6 +128,57 @@ fn main() -> Result<(), anyhow::Error> {
|
|||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
Command::CreateAccount(cmd) => {
|
||||
let client = cmd.rpc.client(Some(&cmd.owner))?;
|
||||
let group = client::pubkey_from_cli(&cmd.group);
|
||||
let owner = client::keypair_from_cli(&cmd.owner);
|
||||
|
||||
let account_num = if let Some(num) = cmd.account_num {
|
||||
num
|
||||
} else {
|
||||
// find free account_num
|
||||
let accounts = MangoClient::find_accounts(&client, group, &owner)?;
|
||||
if accounts.is_empty() {
|
||||
0
|
||||
} else {
|
||||
accounts
|
||||
.iter()
|
||||
.map(|(_, account)| account.fixed.account_num)
|
||||
.max()
|
||||
.unwrap()
|
||||
+ 1
|
||||
}
|
||||
};
|
||||
let (account, txsig) = MangoClient::create_account(
|
||||
&client,
|
||||
group,
|
||||
&owner,
|
||||
&owner,
|
||||
account_num,
|
||||
&cmd.name,
|
||||
)?;
|
||||
println!("{}", account);
|
||||
println!("{}", txsig);
|
||||
}
|
||||
Command::Deposit(cmd) => {
|
||||
let client = cmd.rpc.client(Some(&cmd.owner))?;
|
||||
let account = client::pubkey_from_cli(&cmd.account);
|
||||
let owner = client::keypair_from_cli(&cmd.owner);
|
||||
let mint = client::pubkey_from_cli(&cmd.mint);
|
||||
let client = MangoClient::new_for_existing_account(client, account, owner)?;
|
||||
let txsig = client.token_deposit(mint, cmd.amount)?;
|
||||
println!("{}", txsig);
|
||||
}
|
||||
Command::JupiterSwap(cmd) => {
|
||||
let client = cmd.rpc.client(Some(&cmd.owner))?;
|
||||
let account = client::pubkey_from_cli(&cmd.account);
|
||||
let owner = client::keypair_from_cli(&cmd.owner);
|
||||
let input_mint = client::pubkey_from_cli(&cmd.input_mint);
|
||||
let output_mint = client::pubkey_from_cli(&cmd.output_mint);
|
||||
let client = MangoClient::new_for_existing_account(client, account, owner)?;
|
||||
let txsig = client.jupiter_swap(input_mint, output_mint, cmd.amount, cmd.slippage)?;
|
||||
println!("{}", txsig);
|
||||
}
|
||||
Command::GroupAddress { creator, num } => {
|
||||
let creator = client::pubkey_from_cli(&creator);
|
||||
println!("{}", MangoClient::group_for_admin(creator, num));
|
||||
|
|
|
@ -22,3 +22,10 @@ solana-account-decoder = "~1.10.29"
|
|||
solana-client = "~1.10.29"
|
||||
solana-sdk = "~1.10.29"
|
||||
thiserror = "1.0.31"
|
||||
log = "0.4"
|
||||
reqwest = "0.11.11"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde = "1.0.141"
|
||||
serde_json = "1.0.82"
|
||||
base64 = "0.13.0"
|
||||
bincode = "1.3.3"
|
|
@ -11,17 +11,20 @@ use anchor_lang::Id;
|
|||
use anchor_spl::associated_token::get_associated_token_address;
|
||||
use anchor_spl::token::Token;
|
||||
|
||||
use bincode::Options;
|
||||
use fixed::types::I80F48;
|
||||
use itertools::Itertools;
|
||||
use mango_v4::instructions::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side};
|
||||
use mango_v4::state::{AccountSize, Bank, Group, MangoAccountValue, Serum3MarketIndex, TokenIndex};
|
||||
|
||||
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
|
||||
use solana_client::rpc_client::RpcClient;
|
||||
use solana_sdk::signer::keypair;
|
||||
|
||||
use crate::account_fetcher::*;
|
||||
use crate::context::{MangoGroupContext, Serum3MarketContext, TokenContext};
|
||||
use crate::gpa::fetch_mango_accounts;
|
||||
use crate::jupiter;
|
||||
use crate::util::MyClone;
|
||||
|
||||
use anyhow::Context;
|
||||
|
@ -36,14 +39,21 @@ pub struct Client {
|
|||
pub cluster: Cluster,
|
||||
pub fee_payer: Arc<Keypair>,
|
||||
pub commitment: CommitmentConfig,
|
||||
pub timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(cluster: Cluster, commitment: CommitmentConfig, fee_payer: &Keypair) -> Self {
|
||||
pub fn new(
|
||||
cluster: Cluster,
|
||||
commitment: CommitmentConfig,
|
||||
fee_payer: &Keypair,
|
||||
timeout: Option<Duration>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cluster,
|
||||
fee_payer: Arc::new(fee_payer.clone()),
|
||||
commitment,
|
||||
timeout,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,8 +65,22 @@ impl Client {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn rpc_with_timeout(&self, timeout: Duration) -> RpcClient {
|
||||
RpcClient::new_with_timeout_and_commitment(self.cluster.clone(), timeout, self.commitment)
|
||||
pub fn rpc(&self) -> RpcClient {
|
||||
let url = self.cluster.url().to_string();
|
||||
if let Some(timeout) = self.timeout.as_ref() {
|
||||
RpcClient::new_with_timeout_and_commitment(url, *timeout, self.commitment)
|
||||
} else {
|
||||
RpcClient::new_with_commitment(url, self.commitment)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rpc_async(&self) -> RpcClientAsync {
|
||||
let url = self.cluster.url().to_string();
|
||||
if let Some(timeout) = self.timeout.as_ref() {
|
||||
RpcClientAsync::new_with_timeout_and_commitment(url, *timeout, self.commitment)
|
||||
} else {
|
||||
RpcClientAsync::new_with_commitment(url, self.commitment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,6 +96,19 @@ pub struct MangoClient {
|
|||
pub mango_account_address: Pubkey,
|
||||
|
||||
pub context: MangoGroupContext,
|
||||
|
||||
// Since MangoClient currently provides a blocking interface, we'd prefer to use reqwest::blocking::Client
|
||||
// but that doesn't work inside async contexts. Hence we use the async reqwest Client instead and use
|
||||
// a manual runtime to bridge into async code from both sync and async contexts.
|
||||
// That doesn't work perfectly, see MangoClient::invoke().
|
||||
pub http_client: reqwest::Client,
|
||||
runtime: Option<tokio::runtime::Runtime>,
|
||||
}
|
||||
|
||||
impl Drop for MangoClient {
|
||||
fn drop(&mut self) {
|
||||
self.runtime.take().expect("runtime").shutdown_background();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add retry framework for sending tx and rpc calls
|
||||
|
@ -88,11 +125,20 @@ impl MangoClient {
|
|||
.0
|
||||
}
|
||||
|
||||
pub fn find_accounts(
|
||||
client: &Client,
|
||||
group: Pubkey,
|
||||
owner: &Keypair,
|
||||
) -> anyhow::Result<Vec<(Pubkey, MangoAccountValue)>> {
|
||||
let program = client.anchor_client().program(mango_v4::ID);
|
||||
fetch_mango_accounts(&program, group, owner.pubkey()).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn find_or_create_account(
|
||||
client: &Client,
|
||||
group: Pubkey,
|
||||
owner: Keypair,
|
||||
payer: Keypair, // pays the SOL for the new account
|
||||
owner: &Keypair,
|
||||
payer: &Keypair, // pays the SOL for the new account
|
||||
mango_account_name: &str,
|
||||
) -> anyhow::Result<Pubkey> {
|
||||
let program = client.anchor_client().program(mango_v4::ID);
|
||||
|
@ -113,43 +159,7 @@ impl MangoClient {
|
|||
Some(tuple) => tuple.1.fixed.account_num + 1,
|
||||
None => 0u32,
|
||||
};
|
||||
program
|
||||
.request()
|
||||
.instruction(Instruction {
|
||||
program_id: mango_v4::id(),
|
||||
accounts: anchor_lang::ToAccountMetas::to_account_metas(
|
||||
&mango_v4::accounts::AccountCreate {
|
||||
group,
|
||||
owner: owner.pubkey(),
|
||||
account: {
|
||||
Pubkey::find_program_address(
|
||||
&[
|
||||
group.as_ref(),
|
||||
b"MangoAccount".as_ref(),
|
||||
owner.pubkey().as_ref(),
|
||||
&account_num.to_le_bytes(),
|
||||
],
|
||||
&mango_v4::id(),
|
||||
)
|
||||
.0
|
||||
},
|
||||
payer: payer.pubkey(),
|
||||
system_program: System::id(),
|
||||
},
|
||||
None,
|
||||
),
|
||||
data: anchor_lang::InstructionData::data(
|
||||
&mango_v4::instruction::AccountCreate {
|
||||
account_num,
|
||||
name: mango_account_name.to_owned(),
|
||||
account_size: AccountSize::Small,
|
||||
},
|
||||
),
|
||||
})
|
||||
.signer(&owner)
|
||||
.signer(&payer)
|
||||
.send()
|
||||
.map_err(prettify_client_error)
|
||||
Self::create_account(client, group, owner, payer, account_num, mango_account_name)
|
||||
.context("Failed to create account...")?;
|
||||
}
|
||||
let mango_account_tuples = fetch_mango_accounts(&program, group, owner.pubkey())?;
|
||||
|
@ -160,13 +170,60 @@ impl MangoClient {
|
|||
Ok(mango_account_tuples[index].0)
|
||||
}
|
||||
|
||||
pub fn create_account(
|
||||
client: &Client,
|
||||
group: Pubkey,
|
||||
owner: &Keypair,
|
||||
payer: &Keypair, // pays the SOL for the new account
|
||||
account_num: u32,
|
||||
mango_account_name: &str,
|
||||
) -> anyhow::Result<(Pubkey, Signature)> {
|
||||
let program = client.anchor_client().program(mango_v4::ID);
|
||||
let account = Pubkey::find_program_address(
|
||||
&[
|
||||
group.as_ref(),
|
||||
b"MangoAccount".as_ref(),
|
||||
owner.pubkey().as_ref(),
|
||||
&account_num.to_le_bytes(),
|
||||
],
|
||||
&mango_v4::id(),
|
||||
)
|
||||
.0;
|
||||
let txsig = program
|
||||
.request()
|
||||
.instruction(Instruction {
|
||||
program_id: mango_v4::id(),
|
||||
accounts: anchor_lang::ToAccountMetas::to_account_metas(
|
||||
&mango_v4::accounts::AccountCreate {
|
||||
group,
|
||||
owner: owner.pubkey(),
|
||||
account,
|
||||
payer: payer.pubkey(),
|
||||
system_program: System::id(),
|
||||
},
|
||||
None,
|
||||
),
|
||||
data: anchor_lang::InstructionData::data(&mango_v4::instruction::AccountCreate {
|
||||
account_num,
|
||||
name: mango_account_name.to_owned(),
|
||||
account_size: AccountSize::Small,
|
||||
}),
|
||||
})
|
||||
.signer(owner)
|
||||
.signer(payer)
|
||||
.send()
|
||||
.map_err(prettify_client_error)?;
|
||||
|
||||
Ok((account, txsig))
|
||||
}
|
||||
|
||||
/// Conveniently creates a RPC based client
|
||||
pub fn new_for_existing_account(
|
||||
client: Client,
|
||||
account: Pubkey,
|
||||
owner: Keypair,
|
||||
) -> anyhow::Result<Self> {
|
||||
let rpc = client.rpc_with_timeout(Duration::from_secs(60));
|
||||
let rpc = client.rpc();
|
||||
let account_fetcher = Arc::new(CachedAccountFetcher::new(RpcAccountFetcher { rpc }));
|
||||
let mango_account = account_fetcher_fetch_mango_account(&*account_fetcher, account)?;
|
||||
let group = mango_account.fixed.group;
|
||||
|
@ -199,6 +256,15 @@ impl MangoClient {
|
|||
owner,
|
||||
mango_account_address: account,
|
||||
context: group_context,
|
||||
http_client: reqwest::Client::new(),
|
||||
runtime: Some(
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.thread_name("mango-client")
|
||||
.enable_io()
|
||||
.enable_time()
|
||||
.build()
|
||||
.unwrap(),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -229,13 +295,13 @@ impl MangoClient {
|
|||
|
||||
pub fn derive_health_check_remaining_account_metas(
|
||||
&self,
|
||||
affected_token: Option<TokenIndex>,
|
||||
affected_tokens: Vec<TokenIndex>,
|
||||
writable_banks: bool,
|
||||
) -> anyhow::Result<Vec<AccountMeta>> {
|
||||
let account = self.mango_account()?;
|
||||
self.context.derive_health_check_remaining_account_metas(
|
||||
&account,
|
||||
affected_token,
|
||||
affected_tokens,
|
||||
writable_banks,
|
||||
)
|
||||
}
|
||||
|
@ -273,12 +339,6 @@ impl MangoClient {
|
|||
.chain(account.perp_iter_active_accounts())
|
||||
.map(|&pa| self.context.perp_market_address(pa.market_index));
|
||||
|
||||
let to_account_meta = |pubkey| AccountMeta {
|
||||
pubkey,
|
||||
is_writable: false,
|
||||
is_signer: false,
|
||||
};
|
||||
|
||||
Ok(banks
|
||||
.iter()
|
||||
.map(|(pubkey, is_writable)| AccountMeta {
|
||||
|
@ -286,18 +346,19 @@ impl MangoClient {
|
|||
is_writable: *is_writable,
|
||||
is_signer: false,
|
||||
})
|
||||
.chain(oracles.into_iter().map(to_account_meta))
|
||||
.chain(perp_markets.map(to_account_meta))
|
||||
.chain(serum_oos.map(to_account_meta))
|
||||
.chain(oracles.into_iter().map(to_readonly_account_meta))
|
||||
.chain(perp_markets.map(to_readonly_account_meta))
|
||||
.chain(serum_oos.map(to_readonly_account_meta))
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn token_deposit(&self, token_name: &str, amount: u64) -> anyhow::Result<Signature> {
|
||||
let token_index = *self.context.token_indexes_by_name.get(token_name).unwrap();
|
||||
let mint_info = self.context.mint_info(token_index);
|
||||
pub fn token_deposit(&self, mint: Pubkey, amount: u64) -> anyhow::Result<Signature> {
|
||||
let token = self.context.token_by_mint(&mint)?;
|
||||
let token_index = token.token_index;
|
||||
let mint_info = token.mint_info;
|
||||
|
||||
let health_check_metas =
|
||||
self.derive_health_check_remaining_account_metas(Some(token_index), false)?;
|
||||
self.derive_health_check_remaining_account_metas(vec![token_index], false)?;
|
||||
|
||||
self.program()
|
||||
.request()
|
||||
|
@ -430,7 +491,7 @@ impl MangoClient {
|
|||
let account = self.mango_account()?;
|
||||
let open_orders = account.serum3_find(s3.market_index).unwrap().open_orders;
|
||||
|
||||
let health_check_metas = self.derive_health_check_remaining_account_metas(None, false)?;
|
||||
let health_check_metas = self.derive_health_check_remaining_account_metas(vec![], false)?;
|
||||
|
||||
// https://github.com/project-serum/serum-ts/blob/master/packages/serum/src/market.ts#L1306
|
||||
let limit_price = {
|
||||
|
@ -719,11 +780,7 @@ impl MangoClient {
|
|||
.mint_info
|
||||
.banks()
|
||||
.iter()
|
||||
.map(|bank_pubkey| AccountMeta {
|
||||
pubkey: *bank_pubkey,
|
||||
is_signer: false,
|
||||
is_writable: true,
|
||||
})
|
||||
.map(|bank_pubkey| to_writable_account_meta(*bank_pubkey))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let health_remaining_ams = self
|
||||
|
@ -772,6 +829,223 @@ impl MangoClient {
|
|||
.send()
|
||||
.map_err(prettify_client_error)
|
||||
}
|
||||
|
||||
pub fn jupiter_swap(
|
||||
&self,
|
||||
input_mint: Pubkey,
|
||||
output_mint: Pubkey,
|
||||
source_amount: u64,
|
||||
slippage: f64,
|
||||
) -> anyhow::Result<Signature> {
|
||||
self.invoke(self.jupiter_swap_async(input_mint, output_mint, source_amount, slippage))
|
||||
}
|
||||
|
||||
// Not actually fully async, since it uses the blocking RPC client to send the actual tx
|
||||
pub async fn jupiter_swap_async(
|
||||
&self,
|
||||
input_mint: Pubkey,
|
||||
output_mint: Pubkey,
|
||||
source_amount: u64,
|
||||
slippage: f64,
|
||||
) -> anyhow::Result<Signature> {
|
||||
let source_token = self.context.token_by_mint(&input_mint)?;
|
||||
let target_token = self.context.token_by_mint(&output_mint)?;
|
||||
|
||||
let quote = self
|
||||
.http_client
|
||||
.get("https://quote-api.jup.ag/v1/quote")
|
||||
.query(&[
|
||||
("inputMint", input_mint.to_string()),
|
||||
("outputMint", output_mint.to_string()),
|
||||
("amount", format!("{}", source_amount)),
|
||||
("onlyDirectRoutes", "true".into()),
|
||||
("filterTopNResult", "10".into()),
|
||||
("slippage", format!("{}", slippage)),
|
||||
])
|
||||
.send()
|
||||
.await
|
||||
.context("quote request to jupiter")?
|
||||
.json::<jupiter::QueryResult>()
|
||||
.await
|
||||
.context("receiving json response from jupiter quote request")?;
|
||||
|
||||
// Find the top route that doesn't involve Raydium (that has too many accounts)
|
||||
let route = quote
|
||||
.data
|
||||
.iter()
|
||||
.find(|route| {
|
||||
!route
|
||||
.market_infos
|
||||
.iter()
|
||||
.any(|mi| mi.label.contains("Raydium"))
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"no route for swap. found {} routes, but none were usable",
|
||||
quote.data.len()
|
||||
)
|
||||
})?;
|
||||
|
||||
let swap = self
|
||||
.http_client
|
||||
.post("https://quote-api.jup.ag/v1/swap")
|
||||
.json(&jupiter::SwapRequest {
|
||||
route: route.clone(),
|
||||
user_public_key: self.owner.pubkey().to_string(),
|
||||
wrap_unwrap_sol: false,
|
||||
})
|
||||
.send()
|
||||
.await
|
||||
.context("swap transaction request to jupiter")?
|
||||
.json::<jupiter::SwapResponse>()
|
||||
.await
|
||||
.context("receiving json response from jupiter swap transaction request")?;
|
||||
|
||||
if swap.setup_transaction.is_some() || swap.cleanup_transaction.is_some() {
|
||||
anyhow::bail!(
|
||||
"chosen jupiter route requires setup or cleanup transactions, can't execute"
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: deal with versioned transaction!
|
||||
let jup_tx = bincode::options()
|
||||
.with_fixint_encoding()
|
||||
.reject_trailing_bytes()
|
||||
.deserialize::<solana_sdk::transaction::Transaction>(
|
||||
&base64::decode(&swap.swap_transaction)
|
||||
.context("base64 decoding jupiter transaction")?,
|
||||
)
|
||||
.context("parsing jupiter transaction")?;
|
||||
let jup_ixs = deserialize_instructions(&jup_tx.message)
|
||||
.into_iter()
|
||||
// TODO: possibly creating associated token accounts if they don't exist yet is good?!
|
||||
// we could squeeze the FlashLoan instructions in the middle:
|
||||
// - beginning AToken...
|
||||
// - FlashLoanBegin
|
||||
// - other JUP ix
|
||||
// - FlashLoanEnd
|
||||
// - ending AToken
|
||||
.filter(|ix| {
|
||||
ix.program_id
|
||||
!= Pubkey::from_str("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL").unwrap()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let bank_ams = [
|
||||
source_token.mint_info.first_bank(),
|
||||
target_token.mint_info.first_bank(),
|
||||
]
|
||||
.into_iter()
|
||||
.map(to_writable_account_meta)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let vault_ams = [
|
||||
source_token.mint_info.first_vault(),
|
||||
target_token.mint_info.first_vault(),
|
||||
]
|
||||
.into_iter()
|
||||
.map(to_writable_account_meta)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let token_ams = [source_token.mint_info.mint, target_token.mint_info.mint]
|
||||
.into_iter()
|
||||
.map(|mint| {
|
||||
to_writable_account_meta(
|
||||
anchor_spl::associated_token::get_associated_token_address(
|
||||
&self.owner(),
|
||||
&mint,
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let loan_amounts = vec![source_amount, 0u64];
|
||||
|
||||
// This relies on the fact that health account banks will be identical to the first_bank above!
|
||||
let health_ams = self
|
||||
.derive_health_check_remaining_account_metas(
|
||||
vec![source_token.token_index, target_token.token_index],
|
||||
true,
|
||||
)
|
||||
.context("building health accounts")?;
|
||||
|
||||
let program = self.program();
|
||||
let mut builder = program.request().instruction(Instruction {
|
||||
program_id: mango_v4::id(),
|
||||
accounts: {
|
||||
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
|
||||
&mango_v4::accounts::FlashLoanBegin {
|
||||
group: self.group(),
|
||||
token_program: Token::id(),
|
||||
instructions: solana_sdk::sysvar::instructions::id(),
|
||||
},
|
||||
None,
|
||||
);
|
||||
ams.extend(bank_ams);
|
||||
ams.extend(vault_ams.clone());
|
||||
ams.extend(token_ams.clone());
|
||||
ams
|
||||
},
|
||||
data: anchor_lang::InstructionData::data(&mango_v4::instruction::FlashLoanBegin {
|
||||
loan_amounts,
|
||||
}),
|
||||
});
|
||||
for ix in jup_ixs {
|
||||
builder = builder.instruction(ix);
|
||||
}
|
||||
builder = builder.instruction(Instruction {
|
||||
program_id: mango_v4::id(),
|
||||
accounts: {
|
||||
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
|
||||
&mango_v4::accounts::FlashLoanEnd {
|
||||
account: self.mango_account_address,
|
||||
owner: self.owner(),
|
||||
token_program: Token::id(),
|
||||
},
|
||||
None,
|
||||
);
|
||||
ams.extend(health_ams);
|
||||
ams.extend(vault_ams);
|
||||
ams.extend(token_ams);
|
||||
ams
|
||||
},
|
||||
data: anchor_lang::InstructionData::data(&mango_v4::instruction::FlashLoanEnd {}),
|
||||
});
|
||||
|
||||
let rpc = self.client.rpc_async();
|
||||
builder
|
||||
.signer(&self.owner)
|
||||
.send_rpc_async(&rpc)
|
||||
.await
|
||||
.map_err(prettify_client_error)
|
||||
}
|
||||
|
||||
fn invoke<T, F: std::future::Future<Output = T>>(&self, f: F) -> T {
|
||||
// `block_on()` panics if called within an asynchronous execution context. Whereas
|
||||
// `block_in_place()` only panics if called from a current_thread runtime, which is the
|
||||
// lesser evil.
|
||||
tokio::task::block_in_place(move || self.runtime.as_ref().expect("runtime").block_on(f))
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_instructions(message: &solana_sdk::message::Message) -> Vec<Instruction> {
|
||||
message
|
||||
.instructions
|
||||
.iter()
|
||||
.map(|ci| solana_sdk::instruction::Instruction {
|
||||
program_id: *ci.program_id(&message.account_keys),
|
||||
accounts: ci
|
||||
.accounts
|
||||
.iter()
|
||||
.map(|&index| AccountMeta {
|
||||
pubkey: message.account_keys[index as usize],
|
||||
is_signer: message.is_signer(index.into()),
|
||||
is_writable: message.is_writable(index.into()),
|
||||
})
|
||||
.collect(),
|
||||
data: ci.data.clone(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
struct Serum3Data<'a> {
|
||||
|
@ -835,3 +1109,19 @@ pub fn pubkey_from_cli(pubkey: &str) -> Pubkey {
|
|||
Err(_) => keypair_from_cli(pubkey).pubkey(),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_readonly_account_meta(pubkey: Pubkey) -> AccountMeta {
|
||||
AccountMeta {
|
||||
pubkey,
|
||||
is_writable: false,
|
||||
is_signer: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_writable_account_meta(pubkey: Pubkey) -> AccountMeta {
|
||||
AccountMeta {
|
||||
pubkey,
|
||||
is_writable: true,
|
||||
is_signer: false,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,8 @@ use mango_v4::state::{
|
|||
TokenIndex,
|
||||
};
|
||||
|
||||
use fixed::types::I80F48;
|
||||
|
||||
use crate::gpa::*;
|
||||
|
||||
use solana_sdk::account::Account;
|
||||
|
@ -16,13 +18,21 @@ use solana_sdk::instruction::AccountMeta;
|
|||
use solana_sdk::signature::Keypair;
|
||||
use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TokenContext {
|
||||
pub token_index: TokenIndex,
|
||||
pub name: String,
|
||||
pub mint_info: MintInfo,
|
||||
pub mint_info_address: Pubkey,
|
||||
pub decimals: u8,
|
||||
}
|
||||
|
||||
impl TokenContext {
|
||||
pub fn native_to_ui(&self, native: I80F48) -> f64 {
|
||||
(native / I80F48::from(10u64.pow(self.decimals.into()))).to_num()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Serum3MarketContext {
|
||||
pub address: Pubkey,
|
||||
pub market: Serum3Market,
|
||||
|
@ -68,6 +78,13 @@ impl MangoGroupContext {
|
|||
self.tokens.get(&token_index).unwrap()
|
||||
}
|
||||
|
||||
pub fn token_by_mint(&self, mint: &Pubkey) -> anyhow::Result<&TokenContext> {
|
||||
self.tokens
|
||||
.iter()
|
||||
.find_map(|(_, tc)| (tc.mint_info.mint == *mint).then(|| tc))
|
||||
.ok_or_else(|| anyhow::anyhow!("no token for mint {}", mint))
|
||||
}
|
||||
|
||||
pub fn perp_market_address(&self, perp_market_index: PerpMarketIndex) -> Pubkey {
|
||||
self.perp_markets.get(&perp_market_index).unwrap().address
|
||||
}
|
||||
|
@ -89,6 +106,7 @@ impl MangoGroupContext {
|
|||
(
|
||||
mi.token_index,
|
||||
TokenContext {
|
||||
token_index: mi.token_index,
|
||||
name: String::new(),
|
||||
mint_info: *mi,
|
||||
mint_info_address: *pk,
|
||||
|
@ -187,7 +205,7 @@ impl MangoGroupContext {
|
|||
pub fn derive_health_check_remaining_account_metas(
|
||||
&self,
|
||||
account: &MangoAccountValue,
|
||||
affected_token: Option<TokenIndex>,
|
||||
affected_tokens: Vec<TokenIndex>,
|
||||
writable_banks: bool,
|
||||
) -> anyhow::Result<Vec<AccountMeta>> {
|
||||
// figure out all the banks/oracles that need to be passed for the health check
|
||||
|
@ -198,7 +216,7 @@ impl MangoGroupContext {
|
|||
banks.push(mint_info.first_bank());
|
||||
oracles.push(mint_info.oracle);
|
||||
}
|
||||
if let Some(affected_token_index) = affected_token {
|
||||
for affected_token_index in affected_tokens {
|
||||
if account
|
||||
.token_iter_active()
|
||||
.find(|p| p.token_index == affected_token_index)
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryResult {
|
||||
pub data: Vec<QueryRoute>,
|
||||
pub time_taken: f64,
|
||||
pub context_slot: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryRoute {
|
||||
pub in_amount: u64,
|
||||
pub out_amount: u64,
|
||||
pub amount: u64,
|
||||
pub other_amount_threshold: u64,
|
||||
pub out_amount_with_slippage: u64,
|
||||
pub swap_mode: String,
|
||||
pub price_impact_pct: f64,
|
||||
pub market_infos: Vec<QueryMarketInfo>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryMarketInfo {
|
||||
pub id: String,
|
||||
pub label: String,
|
||||
pub input_mint: String,
|
||||
pub output_mint: String,
|
||||
pub in_amount: u64,
|
||||
pub out_amount: u64,
|
||||
pub lp_fee: QueryFee,
|
||||
pub platform_fee: QueryFee,
|
||||
pub not_enough_liquidity: bool,
|
||||
pub price_impact_pct: f64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryFee {
|
||||
pub amount: u64,
|
||||
pub mint: String,
|
||||
pub pct: f64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SwapRequest {
|
||||
pub route: QueryRoute,
|
||||
pub user_public_key: String,
|
||||
pub wrap_unwrap_sol: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SwapResponse {
|
||||
pub setup_transaction: Option<String>,
|
||||
pub swap_transaction: String,
|
||||
pub cleanup_transaction: Option<String>,
|
||||
}
|
|
@ -9,4 +9,5 @@ mod chain_data_fetcher;
|
|||
mod client;
|
||||
mod context;
|
||||
mod gpa;
|
||||
mod jupiter;
|
||||
mod util;
|
||||
|
|
|
@ -2,6 +2,7 @@ mod crank;
|
|||
mod taker;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anchor_client::Cluster;
|
||||
|
||||
|
@ -75,7 +76,7 @@ fn main() -> Result<(), anyhow::Error> {
|
|||
};
|
||||
|
||||
let mango_client = Arc::new(MangoClient::new_for_existing_account(
|
||||
Client::new(cluster, commitment, &owner),
|
||||
Client::new(cluster, commitment, &owner, Some(Duration::from_secs(1))),
|
||||
cli.mango_account,
|
||||
owner,
|
||||
)?);
|
||||
|
|
|
@ -114,7 +114,7 @@ fn ensure_deposit(mango_client: &Arc<MangoClient>) -> Result<(), anyhow::Error>
|
|||
}
|
||||
|
||||
log::info!("Depositing {} {}", deposit_native, bank.name());
|
||||
mango_client.token_deposit(bank.name(), desired_balance.to_num())?;
|
||||
mango_client.token_deposit(bank.mint, desired_balance.to_num())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -16,7 +16,7 @@ pub fn new_health_cache_(
|
|||
let active_token_len = account.token_iter_active().count();
|
||||
let active_perp_len = account.perp_iter_active_accounts().count();
|
||||
|
||||
let metas = context.derive_health_check_remaining_account_metas(account, None, false)?;
|
||||
let metas = context.derive_health_check_remaining_account_metas(account, vec![], false)?;
|
||||
let accounts = metas
|
||||
.iter()
|
||||
.map(|meta| {
|
||||
|
|
|
@ -99,14 +99,14 @@ async fn main() -> anyhow::Result<()> {
|
|||
let rpc_timeout = Duration::from_secs(1);
|
||||
let cluster = Cluster::Custom(rpc_url.clone(), ws_url.clone());
|
||||
let commitment = CommitmentConfig::processed();
|
||||
let client = Client::new(cluster.clone(), commitment, &liqor_owner);
|
||||
let client = Client::new(cluster.clone(), commitment, &liqor_owner, Some(rpc_timeout));
|
||||
|
||||
// The representation of current on-chain account data
|
||||
let chain_data = Arc::new(RwLock::new(chain_data::ChainData::new()));
|
||||
// Reading accounts from chain_data
|
||||
let account_fetcher = Arc::new(chain_data::AccountFetcher {
|
||||
chain_data: chain_data.clone(),
|
||||
rpc: client.rpc_with_timeout(rpc_timeout),
|
||||
rpc: client.rpc(),
|
||||
});
|
||||
|
||||
let mango_account = account_fetcher.fetch_fresh_mango_account(&cli.liqor_mango_account)?;
|
||||
|
|
Loading…
Reference in New Issue