Skip to main content

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}