wp_solana_core/plan_error.rs
1//! Error types for the plan / fetch / orchestrate layering.
2//!
3//! Each layer has its own `#[non_exhaustive]` enum so that a unit test of
4//! `plan_*` can only ever produce a [`PlanError`], never a [`FetchError`] or
5//! an [`OrchestrateError`]. This is the structural enforcement of the
6//! Phase 1 architectural invariant: the plan layer is I/O-free.
7
8use solana_pubkey::Pubkey;
9
10/// Errors produced by pure planning functions.
11///
12/// Plan-layer code (anything in `crate::plan::*` or
13/// `crate::token::account_planner`) returns this. It MUST NOT contain any
14/// variant that wraps an RPC or async error — if you find yourself wanting
15/// to add `#[from] tokio::io::Error`, the layering is wrong.
16#[derive(thiserror::Error, Debug)]
17#[non_exhaustive]
18pub enum PlanError {
19 /// Caller-supplied liquidity parameter is not usable (e.g. zero amount).
20 #[error("invalid liquidity parameter: {0}")]
21 InvalidLiquidityParam(String),
22
23 /// `wp-solana-amm-math` rejected the quote computation.
24 #[error("liquidity quote computation failed: {0}")]
25 QuoteComputation(String),
26
27 /// `token-account preparation` rejected the supplied state /
28 /// strategy combination (e.g. balance insufficient when the
29 /// global `legacy balance-check global` is on).
30 #[error("token account planning failed: {0}")]
31 TokenAccountPlanning(String),
32
33 /// Token-2022 mint extension data could not be parsed.
34 #[error("Token-2022 mint extension parse failed: {0}")]
35 TokenExtensionParse(String),
36}
37
38/// Errors produced by I/O-bound fetch functions.
39///
40/// Fetch-layer code (anything in `crate::fetch::*` or
41/// `crate::token::account_state`) returns this. Wraps the underlying
42/// `solana-client` errors via `anyhow` so the plan layer never sees them.
43#[derive(thiserror::Error, Debug)]
44#[non_exhaustive]
45pub enum FetchError {
46 /// An underlying RPC call failed.
47 #[error("RPC call failed: {0}")]
48 Rpc(#[from] anyhow::Error),
49
50 /// Caller-supplied fetch input or the target resource is not valid for
51 /// this operation (for example, an unsupported pool type).
52 #[error("invalid fetch input: {0}")]
53 InvalidInput(String),
54
55 /// `get_account` / `get_multiple_accounts` returned `None` for an
56 /// address we expected to exist.
57 #[error("account not found: {0}")]
58 AccountNotFound(Pubkey),
59
60 /// Borsh / SPL deserialization of an account's `data` field failed.
61 #[error("account decode failed: {0}")]
62 AccountDecode(String),
63}
64
65/// Errors produced by orchestrate functions.
66///
67/// Orchestrate-layer code (anything in `crate::orchestrate::*`) returns
68/// this. It is the only layer where `PlanError`, `FetchError`, and
69/// transaction-send errors can meet via `?` propagation.
70///
71/// The variants carry an explicit `"<layer> layer failed:"` prefix in
72/// their `Display` impl rather than using `#[error(transparent)]`. This
73/// trades one redundant word in the error message for the ability to
74/// `grep "fetch layer"` log scans and tell exactly which layer surfaced
75/// the failure — which is the entire selling point of having three
76/// distinct error types in the first place.
77impl From<solana_token_toolkit::TokenError> for FetchError {
78 fn from(err: solana_token_toolkit::TokenError) -> Self {
79 use solana_token_toolkit::TokenError as T;
80 match err {
81 #[cfg(feature = "rpc")]
82 T::Rpc(client_err) => FetchError::Rpc(anyhow::Error::new(client_err)),
83 T::MintNotFound(pk) => FetchError::AccountNotFound(pk),
84 T::MintDecodeFailed { mint, reason } => {
85 FetchError::AccountDecode(format!("mint {mint}: {reason}"))
86 }
87 T::TokenAccountDecodeFailed { token_account, reason } => {
88 FetchError::AccountDecode(format!("token account {token_account}: {reason}"))
89 }
90 T::LengthMismatch { mints, mint_accounts } => FetchError::InvalidInput(format!(
91 "mints/mint_accounts length mismatch: {mints} vs {mint_accounts}"
92 )),
93 T::TransferHookDetected { mint, program } => {
94 FetchError::InvalidInput(format!("transfer hook on mint {mint}: {program}"))
95 }
96 T::WithBalanceNotSupported(pk) => {
97 FetchError::InvalidInput(format!("WithBalance not supported for {pk}"))
98 }
99 err @ T::IncoherentWrapStrategy => FetchError::InvalidInput(err.to_string()),
100 T::InstructionBuild(msg) => {
101 FetchError::InvalidInput(format!("instruction build: {msg}"))
102 }
103 err @ T::InsufficientBalance { .. } => FetchError::InvalidInput(err.to_string()),
104 err @ T::RequireBalanceForSolNotSupported(_) => {
105 FetchError::InvalidInput(err.to_string())
106 }
107 _ => FetchError::InvalidInput(err.to_string()),
108 }
109 }
110}
111
112#[derive(thiserror::Error, Debug)]
113#[non_exhaustive]
114pub enum OrchestrateError {
115 /// Pure plan computation failed before any I/O happened.
116 /// Always indicates a caller-side problem (bad params, math overflow,
117 /// validation rejection) rather than a network or chain issue.
118 #[error("plan layer failed: {0}")]
119 Plan(#[from] PlanError),
120
121 /// I/O fetch step failed before the plan layer ran.
122 /// Indicates an RPC error, missing account, or decode failure when
123 /// loading the snapshot.
124 #[error("fetch layer failed: {0}")]
125 Fetch(#[from] FetchError),
126
127 /// Final transaction send / confirm failed.
128 #[error("transaction send failed: {0}")]
129 Send(#[source] anyhow::Error),
130}