wp-solana-core 0.1.2

Cross-protocol shared core for waterpump-solana: token planning compatibility, error types, system constants.
Documentation
//! Error types for the plan / fetch / orchestrate layering.
//!
//! Each layer has its own `#[non_exhaustive]` enum so that a unit test of
//! `plan_*` can only ever produce a [`PlanError`], never a [`FetchError`] or
//! an [`OrchestrateError`]. This is the structural enforcement of the
//! Phase 1 architectural invariant: the plan layer is I/O-free.

use solana_pubkey::Pubkey;

/// Errors produced by pure planning functions.
///
/// Plan-layer code (anything in `crate::plan::*` or
/// `crate::token::account_planner`) returns this. It MUST NOT contain any
/// variant that wraps an RPC or async error — if you find yourself wanting
/// to add `#[from] tokio::io::Error`, the layering is wrong.
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum PlanError {
    /// Caller-supplied liquidity parameter is not usable (e.g. zero amount).
    #[error("invalid liquidity parameter: {0}")]
    InvalidLiquidityParam(String),

    /// `wp-solana-amm-math` rejected the quote computation.
    #[error("liquidity quote computation failed: {0}")]
    QuoteComputation(String),

    /// `token-account preparation` rejected the supplied state /
    /// strategy combination (e.g. balance insufficient when the
    /// global `legacy balance-check global` is on).
    #[error("token account planning failed: {0}")]
    TokenAccountPlanning(String),

    /// Token-2022 mint extension data could not be parsed.
    #[error("Token-2022 mint extension parse failed: {0}")]
    TokenExtensionParse(String),
}

/// Errors produced by I/O-bound fetch functions.
///
/// Fetch-layer code (anything in `crate::fetch::*` or
/// `crate::token::account_state`) returns this. Wraps the underlying
/// `solana-client` errors via `anyhow` so the plan layer never sees them.
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum FetchError {
    /// An underlying RPC call failed.
    #[error("RPC call failed: {0}")]
    Rpc(#[from] anyhow::Error),

    /// Caller-supplied fetch input or the target resource is not valid for
    /// this operation (for example, an unsupported pool type).
    #[error("invalid fetch input: {0}")]
    InvalidInput(String),

    /// `get_account` / `get_multiple_accounts` returned `None` for an
    /// address we expected to exist.
    #[error("account not found: {0}")]
    AccountNotFound(Pubkey),

    /// Borsh / SPL deserialization of an account's `data` field failed.
    #[error("account decode failed: {0}")]
    AccountDecode(String),
}

/// Errors produced by orchestrate functions.
///
/// Orchestrate-layer code (anything in `crate::orchestrate::*`) returns
/// this. It is the only layer where `PlanError`, `FetchError`, and
/// transaction-send errors can meet via `?` propagation.
///
/// The variants carry an explicit `"<layer> layer failed:"` prefix in
/// their `Display` impl rather than using `#[error(transparent)]`. This
/// trades one redundant word in the error message for the ability to
/// `grep "fetch layer"` log scans and tell exactly which layer surfaced
/// the failure — which is the entire selling point of having three
/// distinct error types in the first place.
impl From<solana_token_toolkit::TokenError> for FetchError {
    fn from(err: solana_token_toolkit::TokenError) -> Self {
        use solana_token_toolkit::TokenError as T;
        match err {
            #[cfg(feature = "rpc")]
            T::Rpc(client_err) => FetchError::Rpc(anyhow::Error::new(client_err)),
            T::MintNotFound(pk) => FetchError::AccountNotFound(pk),
            T::MintDecodeFailed { mint, reason } => {
                FetchError::AccountDecode(format!("mint {mint}: {reason}"))
            }
            T::TokenAccountDecodeFailed { token_account, reason } => {
                FetchError::AccountDecode(format!("token account {token_account}: {reason}"))
            }
            T::LengthMismatch { mints, mint_accounts } => FetchError::InvalidInput(format!(
                "mints/mint_accounts length mismatch: {mints} vs {mint_accounts}"
            )),
            T::TransferHookDetected { mint, program } => {
                FetchError::InvalidInput(format!("transfer hook on mint {mint}: {program}"))
            }
            T::WithBalanceNotSupported(pk) => {
                FetchError::InvalidInput(format!("WithBalance not supported for {pk}"))
            }
            err @ T::IncoherentWrapStrategy => FetchError::InvalidInput(err.to_string()),
            T::InstructionBuild(msg) => {
                FetchError::InvalidInput(format!("instruction build: {msg}"))
            }
            err @ T::InsufficientBalance { .. } => FetchError::InvalidInput(err.to_string()),
            err @ T::RequireBalanceForSolNotSupported(_) => {
                FetchError::InvalidInput(err.to_string())
            }
            _ => FetchError::InvalidInput(err.to_string()),
        }
    }
}

#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum OrchestrateError {
    /// Pure plan computation failed before any I/O happened.
    /// Always indicates a caller-side problem (bad params, math overflow,
    /// validation rejection) rather than a network or chain issue.
    #[error("plan layer failed: {0}")]
    Plan(#[from] PlanError),

    /// I/O fetch step failed before the plan layer ran.
    /// Indicates an RPC error, missing account, or decode failure when
    /// loading the snapshot.
    #[error("fetch layer failed: {0}")]
    Fetch(#[from] FetchError),

    /// Final transaction send / confirm failed.
    #[error("transaction send failed: {0}")]
    Send(#[source] anyhow::Error),
}