1use solana_address::Address;
25use solana_instruction::Instruction;
26
27use super::{Fetched, FetchedAccount, HookError, HookState, LegExecute, Step};
28use crate::utils::{
29 MintInfo, TradeAccounts, TradeDirection, TradeLegInput, TradeMints,
30 TradeMode, check_base_mint, legs_of_trade,
31};
32use crate::{dex, launchpad};
33
34#[derive(Clone, Debug, PartialEq, Eq)]
36#[non_exhaustive]
37pub enum Unroutable {
38 Hook(HookError),
39 NotCached(Vec<Address>),
42 UncacheableRead,
46 ExtraSigns,
49 ExecuteRoleConflict,
55 WritableRepeat(Address),
64 MintMismatch(Address),
66 BaseNotToken2022(Address),
68 QuoteTokenProgramMismatch(Address),
71 HookNotRead(Address),
75}
76
77impl std::fmt::Display for Unroutable {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 match self {
80 Self::Hook(error) => error.fmt(f),
81 Self::NotCached(_) => {
82 write!(f, "transfer hook accounts not cached")
83 }
84 Self::UncacheableRead => write!(
85 f,
86 "a transfer hook reads an account that depends on the trader"
87 ),
88 Self::ExtraSigns => {
89 write!(f, "a transfer hook extra must sign")
90 }
91 Self::ExecuteRoleConflict => write!(
92 f,
93 "a transfer hook wants one account of its Execute both \
94 read-only and writable"
95 ),
96 Self::WritableRepeat(account) => write!(
97 f,
98 "a transfer hook wants {account} writable, which the trade \
99 holds read-only"
100 ),
101 Self::MintMismatch(mint) => write!(
102 f,
103 "the mint info is not the info of the trade's mint {mint}"
104 ),
105 Self::BaseNotToken2022(mint) => {
106 write!(f, "the base mint {mint} is not a Token-2022 mint")
107 }
108 Self::QuoteTokenProgramMismatch(mint) => write!(
109 f,
110 "the quote token program is not the token program of the quote \
111 mint {mint}"
112 ),
113 Self::HookNotRead(mint) => {
114 write!(f, "the mint {mint} has a hook program and no hook")
115 }
116 }
117 }
118}
119
120impl std::error::Error for Unroutable {}
121
122impl From<HookError> for Unroutable {
123 fn from(error: HookError) -> Self {
124 Self::Hook(error)
125 }
126}
127
128#[derive(Clone, Debug, PartialEq, Eq)]
131pub struct Route {
132 pub instruction: Instruction,
133 pub reads: Vec<Address>,
134}
135
136#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
138pub enum TradeProgram {
139 Launchpad,
140 Dex,
141}
142
143#[derive(Clone, Copy, Debug)]
145pub struct TradeRoute<'r> {
146 pub program: TradeProgram,
147 pub trade: &'r TradeAccounts,
148 pub base_mint_info: &'r MintInfo,
150 pub quote_mint_info: &'r MintInfo,
152 pub direction: TradeDirection,
153 pub mode: TradeMode,
154 pub amount: u64,
156 pub limit: u64,
158}
159
160impl TradeRoute<'_> {
161 fn build(&self) -> (Instruction, super::HookLegs) {
163 let input = TradeLegInput {
164 accounts: *self.trade,
165 direction: self.direction,
166 };
167 match self.program {
168 TradeProgram::Launchpad => (
169 launchpad::trade::trade_instruction(
170 self.trade,
171 launchpad::trade::TradeArgs {
172 direction: self.direction,
173 mode: self.mode,
174 amount: self.amount,
175 limit: self.limit,
176 },
177 ),
178 legs_of_trade(&input, launchpad::trade::WRITES_MARKET),
179 ),
180 TradeProgram::Dex => (
181 dex::trade::trade_instruction(
182 self.trade,
183 dex::trade::TradeArgs {
184 direction: self.direction.into(),
185 mode: self.mode,
186 amount: self.amount,
187 limit: self.limit,
188 },
189 ),
190 legs_of_trade(&input, dex::trade::WRITES_MARKET),
191 ),
192 }
193 }
194}
195
196const DEMOTED: [Address; 31] = [
199 solana_address::address!("11111111111111111111111111111111"),
200 solana_address::address!("NativeLoader1111111111111111111111111111111"),
201 solana_address::address!("BPFLoader1111111111111111111111111111111111"),
202 solana_address::address!("BPFLoader2111111111111111111111111111111111"),
203 solana_address::address!("BPFLoaderUpgradeab1e11111111111111111111111"),
204 solana_address::address!("LoaderV411111111111111111111111111111111111"),
205 solana_address::address!("ComputeBudget111111111111111111111111111111"),
206 solana_address::address!("AddressLookupTab1e1111111111111111111111111"),
207 solana_address::address!("Config1111111111111111111111111111111111111"),
208 solana_address::address!("Stake11111111111111111111111111111111111111"),
209 solana_address::address!("StakeConfig11111111111111111111111111111111"),
210 solana_address::address!("Vote111111111111111111111111111111111111111"),
211 solana_address::address!("Ed25519SigVerify111111111111111111111111111"),
212 solana_address::address!("KeccakSecp256k11111111111111111111111111111"),
213 solana_address::address!("Secp256r1SigVerify1111111111111111111111111"),
214 solana_address::address!("Feature111111111111111111111111111111111111"),
215 solana_address::address!("ZkE1Gama1Proof11111111111111111111111111111"),
216 solana_address::address!("ZkTokenProof1111111111111111111111111111111"),
217 solana_address::address!("Sysvar1111111111111111111111111111111111111"),
218 solana_address::address!("SysvarC1ock11111111111111111111111111111111"),
219 solana_address::address!("SysvarEpochRewards1111111111111111111111111"),
220 solana_address::address!("SysvarEpochSchedu1e111111111111111111111111"),
221 solana_address::address!("SysvarFees111111111111111111111111111111111"),
222 solana_address::address!("Sysvar1nstructions1111111111111111111111111"),
223 solana_address::address!("SysvarLastRestartS1ot1111111111111111111111"),
224 solana_address::address!("SysvarRecentB1ockHashes11111111111111111111"),
225 solana_address::address!("SysvarRent111111111111111111111111111111111"),
226 solana_address::address!("SysvarRewards111111111111111111111111111111"),
227 solana_address::address!("SysvarS1otHashes111111111111111111111111111"),
228 solana_address::address!("SysvarS1otHistory11111111111111111111111111"),
229 solana_address::address!("SysvarStakeHistory1111111111111111111111111"),
230];
231
232pub fn trade<'a>(
242 route: TradeRoute<'_>,
243 accounts: impl Fn(&Address) -> Fetched<'a>,
244) -> Result<Route, Unroutable> {
245 let trade = route.trade;
246 let mints = TradeMints {
247 base: route.base_mint_info,
248 quote: route.quote_mint_info,
249 };
250 check_mints(&route)?;
251 let (mut instruction, legs) = route.build();
252 let resolved = match super::resolve_market(mints, &legs, accounts)? {
253 Step::Done(resolved) => resolved,
254 Step::Fetch(addresses) => return Err(Unroutable::NotCached(addresses)),
255 };
256 for execute in &resolved.executes {
257 screen_execute(execute)?;
258 }
259 let read_only = [
260 trade.market,
261 trade.base_mint,
262 trade.quote_mint,
263 trade.partner_config,
264 ];
265 if let Some(meta) = resolved.accounts.iter().find(|meta| {
266 meta.is_writable
267 && (read_only.contains(&meta.pubkey)
268 || DEMOTED.contains(&meta.pubkey))
269 }) {
270 return Err(Unroutable::WritableRepeat(meta.pubkey));
271 }
272 instruction.accounts.extend(resolved.accounts);
273 Ok(Route {
274 instruction,
275 reads: resolved.reads,
276 })
277}
278
279#[derive(Clone, Debug, PartialEq, Eq)]
281#[non_exhaustive]
282pub struct Screen {
283 pub route: Result<(), Unroutable>,
285 pub not_cached: Vec<Address>,
288 pub stand_ins: [Result<Route, Unroutable>; 2],
290}
291
292const STAND_INS: [u8; 2] = [1, 5];
295
296const fn stand_in(trade: &TradeAccounts, seed: u8) -> TradeAccounts {
300 TradeAccounts {
301 user: Address::new_from_array([seed; 32]),
302 payer: Address::new_from_array([seed.wrapping_add(1); 32]),
303 user_base_account: Address::new_from_array([seed.wrapping_add(2); 32]),
304 user_quote_account: Address::new_from_array([seed.wrapping_add(3); 32]),
305 ..*trade
306 }
307}
308
309pub fn screen<'a>(
323 route: TradeRoute<'_>,
324 accounts: impl Fn(&Address) -> Fetched<'a>,
325) -> Screen {
326 let stand_ins = STAND_INS.map(|seed| {
327 trade(
328 TradeRoute {
329 trade: &stand_in(route.trade, seed),
330 ..route
331 },
332 &accounts,
333 )
334 });
335 let (route, not_cached) = match &stand_ins {
336 [Ok(first), Ok(second)] => (
337 if same(&first.reads, &second.reads) {
338 Ok(())
339 } else {
340 Err(Unroutable::UncacheableRead)
341 },
342 Vec::new(),
343 ),
344 [
345 Err(Unroutable::NotCached(first)),
346 Err(Unroutable::NotCached(second)),
347 ] if same(first, second) => {
348 (Err(Unroutable::NotCached(first.clone())), first.clone())
349 }
350 [Err(first), Err(second)] if first == second => {
351 (Err(first.clone()), Vec::new())
352 }
353 _ => (Err(Unroutable::UncacheableRead), Vec::new()),
354 };
355 Screen {
356 route,
357 not_cached,
358 stand_ins,
359 }
360}
361
362const DIRECTIONS: [TradeDirection; 2] =
364 [TradeDirection::Buy, TradeDirection::Sell];
365
366#[derive(Clone, Debug)]
379pub struct MarketLoad {
380 program: TradeProgram,
381 market: TradeAccounts,
382 mints: [MintInfo; 2],
384 cache: HookState,
386 named: Vec<Address>,
388}
389
390#[derive(Clone, Debug)]
392pub struct MarketLoadArgs {
393 pub program: TradeProgram,
394 pub market: TradeAccounts,
397 pub base_mint_info: MintInfo,
399 pub quote_mint_info: MintInfo,
400}
401
402impl MarketLoad {
403 #[must_use]
404 pub fn new(args: MarketLoadArgs) -> Self {
405 Self {
406 program: args.program,
407 market: args.market,
408 mints: [args.base_mint_info, args.quote_mint_info],
409 cache: HookState::default(),
410 named: Vec::new(),
411 }
412 }
413
414 pub fn missing(&mut self) -> Result<Vec<Address>, HookError> {
422 let mut next: Vec<Address> = Vec::new();
423 for info in &self.mints {
424 push_new(&mut next, &info.missing()?);
425 }
426 for screen in self.screens().iter().flatten() {
427 push_new(&mut next, &screen.not_cached);
428 }
429 next.retain(|address| !self.named.contains(address));
430 push_new(&mut self.named, &next);
431 Ok(next)
432 }
433
434 pub fn insert(
437 &mut self,
438 address: Address,
439 account: Option<FetchedAccount>,
440 ) {
441 for info in &mut self.mints {
442 if info
443 .missing()
444 .is_ok_and(|missing| missing.contains(&address))
445 {
446 info.insert(address, account.clone());
447 }
448 }
449 self.cache.insert(address, account);
450 }
451
452 #[must_use]
453 pub fn finish(self) -> LoadedMarket {
454 let screens = self.screens();
455 let [base_mint_info, quote_mint_info] = self.mints;
456 LoadedMarket {
457 base_mint_info,
458 quote_mint_info,
459 cache: self.cache,
460 screens,
461 declare: self.named,
462 }
463 }
464
465 fn screens(&self) -> Option<[Screen; 2]> {
468 let [base_mint_info, quote_mint_info] = &self.mints;
469 if base_mint_info.hook_program.is_none()
470 && quote_mint_info.hook_program.is_none()
471 {
472 return None;
473 }
474 Some(DIRECTIONS.map(|direction| {
475 screen(
476 TradeRoute {
477 program: self.program,
478 trade: &self.market,
479 base_mint_info,
480 quote_mint_info,
481 direction,
482 mode: TradeMode::ExactIn,
484 amount: 0,
486 limit: 0,
487 },
488 |address| self.cache.fetched(address),
489 )
490 }))
491 }
492}
493
494#[derive(Clone, Debug, PartialEq, Eq)]
496#[non_exhaustive]
497pub struct LoadedMarket {
498 pub base_mint_info: MintInfo,
499 pub quote_mint_info: MintInfo,
500 pub cache: HookState,
503 pub screens: Option<[Screen; 2]>,
507 pub declare: Vec<Address>,
509}
510
511fn push_new(all: &mut Vec<Address>, keys: &[Address]) {
512 for key in keys {
513 if !all.contains(key) {
514 all.push(*key);
515 }
516 }
517}
518
519fn same(first: &[Address], second: &[Address]) -> bool {
521 first.len() == second.len() && first.iter().all(|key| second.contains(key))
522}
523
524fn check_mints(route: &TradeRoute<'_>) -> Result<(), Unroutable> {
527 let infos = [
528 (route.trade.base_mint, route.base_mint_info),
529 (route.trade.quote_mint, route.quote_mint_info),
530 ];
531 if let Some((mint, _)) =
532 infos.iter().find(|(mint, info)| info.address != *mint)
533 {
534 return Err(Unroutable::MintMismatch(*mint));
535 }
536 if check_base_mint(route.base_mint_info).is_err() {
537 return Err(Unroutable::BaseNotToken2022(route.trade.base_mint));
538 }
539 if route.trade.quote_token_program != route.quote_mint_info.token_program {
540 return Err(Unroutable::QuoteTokenProgramMismatch(
541 route.trade.quote_mint,
542 ));
543 }
544 if let Some((mint, _)) = infos.iter().find(|(_, info)| info.hook_not_read())
545 {
546 return Err(Unroutable::HookNotRead(*mint));
547 }
548 Ok(())
549}
550
551fn screen_execute(execute: &LegExecute) -> Result<(), Unroutable> {
555 let extras = execute.metas.iter().skip(super::EXECUTE_ACCOUNTS);
556 for (meta, (signer, writable)) in extras.zip(&execute.declared) {
557 if *signer {
558 return Err(Unroutable::ExtraSigns);
559 }
560 let held = execute
561 .metas
562 .iter()
563 .any(|other| other.pubkey == meta.pubkey && other.is_writable);
564 if held != *writable {
565 return Err(Unroutable::ExecuteRoleConflict);
566 }
567 }
568 Ok(())
569}
570
571#[cfg(test)]
572mod tests;