use solana_address::Address;
use solana_instruction::Instruction;
use super::{Fetched, FetchedAccount, HookError, HookState, LegExecute, Step};
use crate::utils::{
MintInfo, TradeAccounts, TradeDirection, TradeLegInput, TradeMints,
TradeMode, check_base_mint, legs_of_trade,
};
use crate::{dex, launchpad};
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Unroutable {
Hook(HookError),
NotCached(Vec<Address>),
UncacheableRead,
ExtraSigns,
ExecuteRoleConflict,
WritableRepeat(Address),
MintMismatch(Address),
BaseNotToken2022(Address),
QuoteTokenProgramMismatch(Address),
HookNotRead(Address),
}
impl std::fmt::Display for Unroutable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Hook(error) => error.fmt(f),
Self::NotCached(_) => {
write!(f, "transfer hook accounts not cached")
}
Self::UncacheableRead => write!(
f,
"a transfer hook reads an account that depends on the trader"
),
Self::ExtraSigns => {
write!(f, "a transfer hook extra must sign")
}
Self::ExecuteRoleConflict => write!(
f,
"a transfer hook wants one account of its Execute both \
read-only and writable"
),
Self::WritableRepeat(account) => write!(
f,
"a transfer hook wants {account} writable, which the trade \
holds read-only"
),
Self::MintMismatch(mint) => write!(
f,
"the mint info is not the info of the trade's mint {mint}"
),
Self::BaseNotToken2022(mint) => {
write!(f, "the base mint {mint} is not a Token-2022 mint")
}
Self::QuoteTokenProgramMismatch(mint) => write!(
f,
"the quote token program is not the token program of the quote \
mint {mint}"
),
Self::HookNotRead(mint) => {
write!(f, "the mint {mint} has a hook program and no hook")
}
}
}
}
impl std::error::Error for Unroutable {}
impl From<HookError> for Unroutable {
fn from(error: HookError) -> Self {
Self::Hook(error)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Route {
pub instruction: Instruction,
pub reads: Vec<Address>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum TradeProgram {
Launchpad,
Dex,
}
#[derive(Clone, Copy, Debug)]
pub struct TradeRoute<'r> {
pub program: TradeProgram,
pub trade: &'r TradeAccounts,
pub base_mint_info: &'r MintInfo,
pub quote_mint_info: &'r MintInfo,
pub direction: TradeDirection,
pub mode: TradeMode,
pub amount: u64,
pub limit: u64,
}
impl TradeRoute<'_> {
fn build(&self) -> (Instruction, super::HookLegs) {
let input = TradeLegInput {
accounts: *self.trade,
direction: self.direction,
};
match self.program {
TradeProgram::Launchpad => (
launchpad::trade::trade_instruction(
self.trade,
launchpad::trade::TradeArgs {
direction: self.direction,
mode: self.mode,
amount: self.amount,
limit: self.limit,
},
),
legs_of_trade(&input, launchpad::trade::WRITES_MARKET),
),
TradeProgram::Dex => (
dex::trade::trade_instruction(
self.trade,
dex::trade::TradeArgs {
direction: self.direction.into(),
mode: self.mode,
amount: self.amount,
limit: self.limit,
},
),
legs_of_trade(&input, dex::trade::WRITES_MARKET),
),
}
}
}
const DEMOTED: [Address; 31] = [
solana_address::address!("11111111111111111111111111111111"),
solana_address::address!("NativeLoader1111111111111111111111111111111"),
solana_address::address!("BPFLoader1111111111111111111111111111111111"),
solana_address::address!("BPFLoader2111111111111111111111111111111111"),
solana_address::address!("BPFLoaderUpgradeab1e11111111111111111111111"),
solana_address::address!("LoaderV411111111111111111111111111111111111"),
solana_address::address!("ComputeBudget111111111111111111111111111111"),
solana_address::address!("AddressLookupTab1e1111111111111111111111111"),
solana_address::address!("Config1111111111111111111111111111111111111"),
solana_address::address!("Stake11111111111111111111111111111111111111"),
solana_address::address!("StakeConfig11111111111111111111111111111111"),
solana_address::address!("Vote111111111111111111111111111111111111111"),
solana_address::address!("Ed25519SigVerify111111111111111111111111111"),
solana_address::address!("KeccakSecp256k11111111111111111111111111111"),
solana_address::address!("Secp256r1SigVerify1111111111111111111111111"),
solana_address::address!("Feature111111111111111111111111111111111111"),
solana_address::address!("ZkE1Gama1Proof11111111111111111111111111111"),
solana_address::address!("ZkTokenProof1111111111111111111111111111111"),
solana_address::address!("Sysvar1111111111111111111111111111111111111"),
solana_address::address!("SysvarC1ock11111111111111111111111111111111"),
solana_address::address!("SysvarEpochRewards1111111111111111111111111"),
solana_address::address!("SysvarEpochSchedu1e111111111111111111111111"),
solana_address::address!("SysvarFees111111111111111111111111111111111"),
solana_address::address!("Sysvar1nstructions1111111111111111111111111"),
solana_address::address!("SysvarLastRestartS1ot1111111111111111111111"),
solana_address::address!("SysvarRecentB1ockHashes11111111111111111111"),
solana_address::address!("SysvarRent111111111111111111111111111111111"),
solana_address::address!("SysvarRewards111111111111111111111111111111"),
solana_address::address!("SysvarS1otHashes111111111111111111111111111"),
solana_address::address!("SysvarS1otHistory11111111111111111111111111"),
solana_address::address!("SysvarStakeHistory1111111111111111111111111"),
];
pub fn trade<'a>(
route: TradeRoute<'_>,
accounts: impl Fn(&Address) -> Fetched<'a>,
) -> Result<Route, Unroutable> {
let trade = route.trade;
let mints = TradeMints {
base: route.base_mint_info,
quote: route.quote_mint_info,
};
check_mints(&route)?;
let (mut instruction, legs) = route.build();
let resolved = match super::resolve_market(mints, &legs, accounts)? {
Step::Done(resolved) => resolved,
Step::Fetch(addresses) => return Err(Unroutable::NotCached(addresses)),
};
for execute in &resolved.executes {
screen_execute(execute)?;
}
let read_only = [
trade.market,
trade.base_mint,
trade.quote_mint,
trade.partner_config,
];
if let Some(meta) = resolved.accounts.iter().find(|meta| {
meta.is_writable
&& (read_only.contains(&meta.pubkey)
|| DEMOTED.contains(&meta.pubkey))
}) {
return Err(Unroutable::WritableRepeat(meta.pubkey));
}
instruction.accounts.extend(resolved.accounts);
Ok(Route {
instruction,
reads: resolved.reads,
})
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct Screen {
pub route: Result<(), Unroutable>,
pub not_cached: Vec<Address>,
pub stand_ins: [Result<Route, Unroutable>; 2],
}
const STAND_INS: [u8; 2] = [1, 5];
const fn stand_in(trade: &TradeAccounts, seed: u8) -> TradeAccounts {
TradeAccounts {
user: Address::new_from_array([seed; 32]),
payer: Address::new_from_array([seed.wrapping_add(1); 32]),
user_base_account: Address::new_from_array([seed.wrapping_add(2); 32]),
user_quote_account: Address::new_from_array([seed.wrapping_add(3); 32]),
..*trade
}
}
pub fn screen<'a>(
route: TradeRoute<'_>,
accounts: impl Fn(&Address) -> Fetched<'a>,
) -> Screen {
let stand_ins = STAND_INS.map(|seed| {
trade(
TradeRoute {
trade: &stand_in(route.trade, seed),
..route
},
&accounts,
)
});
let (route, not_cached) = match &stand_ins {
[Ok(first), Ok(second)] => (
if same(&first.reads, &second.reads) {
Ok(())
} else {
Err(Unroutable::UncacheableRead)
},
Vec::new(),
),
[
Err(Unroutable::NotCached(first)),
Err(Unroutable::NotCached(second)),
] if same(first, second) => {
(Err(Unroutable::NotCached(first.clone())), first.clone())
}
[Err(first), Err(second)] if first == second => {
(Err(first.clone()), Vec::new())
}
_ => (Err(Unroutable::UncacheableRead), Vec::new()),
};
Screen {
route,
not_cached,
stand_ins,
}
}
const DIRECTIONS: [TradeDirection; 2] =
[TradeDirection::Buy, TradeDirection::Sell];
#[derive(Clone, Debug)]
pub struct MarketLoad {
program: TradeProgram,
market: TradeAccounts,
mints: [MintInfo; 2],
cache: HookState,
named: Vec<Address>,
}
#[derive(Clone, Debug)]
pub struct MarketLoadArgs {
pub program: TradeProgram,
pub market: TradeAccounts,
pub base_mint_info: MintInfo,
pub quote_mint_info: MintInfo,
}
impl MarketLoad {
#[must_use]
pub fn new(args: MarketLoadArgs) -> Self {
Self {
program: args.program,
market: args.market,
mints: [args.base_mint_info, args.quote_mint_info],
cache: HookState::default(),
named: Vec::new(),
}
}
pub fn missing(&mut self) -> Result<Vec<Address>, HookError> {
let mut next: Vec<Address> = Vec::new();
for info in &self.mints {
push_new(&mut next, &info.missing()?);
}
for screen in self.screens().iter().flatten() {
push_new(&mut next, &screen.not_cached);
}
next.retain(|address| !self.named.contains(address));
push_new(&mut self.named, &next);
Ok(next)
}
pub fn insert(
&mut self,
address: Address,
account: Option<FetchedAccount>,
) {
for info in &mut self.mints {
if info
.missing()
.is_ok_and(|missing| missing.contains(&address))
{
info.insert(address, account.clone());
}
}
self.cache.insert(address, account);
}
#[must_use]
pub fn finish(self) -> LoadedMarket {
let screens = self.screens();
let [base_mint_info, quote_mint_info] = self.mints;
LoadedMarket {
base_mint_info,
quote_mint_info,
cache: self.cache,
screens,
declare: self.named,
}
}
fn screens(&self) -> Option<[Screen; 2]> {
let [base_mint_info, quote_mint_info] = &self.mints;
if base_mint_info.hook_program.is_none()
&& quote_mint_info.hook_program.is_none()
{
return None;
}
Some(DIRECTIONS.map(|direction| {
screen(
TradeRoute {
program: self.program,
trade: &self.market,
base_mint_info,
quote_mint_info,
direction,
mode: TradeMode::ExactIn,
amount: 0,
limit: 0,
},
|address| self.cache.fetched(address),
)
}))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct LoadedMarket {
pub base_mint_info: MintInfo,
pub quote_mint_info: MintInfo,
pub cache: HookState,
pub screens: Option<[Screen; 2]>,
pub declare: Vec<Address>,
}
fn push_new(all: &mut Vec<Address>, keys: &[Address]) {
for key in keys {
if !all.contains(key) {
all.push(*key);
}
}
}
fn same(first: &[Address], second: &[Address]) -> bool {
first.len() == second.len() && first.iter().all(|key| second.contains(key))
}
fn check_mints(route: &TradeRoute<'_>) -> Result<(), Unroutable> {
let infos = [
(route.trade.base_mint, route.base_mint_info),
(route.trade.quote_mint, route.quote_mint_info),
];
if let Some((mint, _)) =
infos.iter().find(|(mint, info)| info.address != *mint)
{
return Err(Unroutable::MintMismatch(*mint));
}
if check_base_mint(route.base_mint_info).is_err() {
return Err(Unroutable::BaseNotToken2022(route.trade.base_mint));
}
if route.trade.quote_token_program != route.quote_mint_info.token_program {
return Err(Unroutable::QuoteTokenProgramMismatch(
route.trade.quote_mint,
));
}
if let Some((mint, _)) = infos.iter().find(|(_, info)| info.hook_not_read())
{
return Err(Unroutable::HookNotRead(*mint));
}
Ok(())
}
fn screen_execute(execute: &LegExecute) -> Result<(), Unroutable> {
let extras = execute.metas.iter().skip(super::EXECUTE_ACCOUNTS);
for (meta, (signer, writable)) in extras.zip(&execute.declared) {
if *signer {
return Err(Unroutable::ExtraSigns);
}
let held = execute
.metas
.iter()
.any(|other| other.pubkey == meta.pubkey && other.is_writable);
if held != *writable {
return Err(Unroutable::ExecuteRoleConflict);
}
}
Ok(())
}
#[cfg(test)]
mod tests;