Client: Add jupiter_swap (#139)

This commit is contained in:
Christian Kamm 2022-08-04 17:01:00 +02:00 committed by GitHub
parent 331bb7ebf0
commit 681c69e3a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 611 additions and 93 deletions

37
Cargo.lock generated
View File

@ -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

@ -1 +1 @@
Subproject commit 442c00634e847af84672727dc00b45a2c8d0a956
Subproject commit 2058b6461cb0de5af90b04eb8fae4225a368251e

View File

@ -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));

View File

@ -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"

View File

@ -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,
}
}

View File

@ -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)

61
client/src/jupiter.rs Normal file
View File

@ -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>,
}

View File

@ -9,4 +9,5 @@ mod chain_data_fetcher;
mod client;
mod context;
mod gpa;
mod jupiter;
mod util;

View File

@ -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,
)?);

View File

@ -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(())

View File

@ -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| {

View File

@ -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)?;