tally_sdk/
error.rs

1//! Error types for the Tally SDK
2//!
3//! This module provides comprehensive error handling for the Tally SDK, including
4//! automatic mapping of program-specific error codes to meaningful error variants.
5//!
6//! # Program Error Mapping
7//!
8//! The SDK automatically maps specific program error codes to detailed error variants:
9//!
10//! - **6012**: `InvalidSubscriberTokenAccount` - Invalid subscriber USDC token account
11//! - **6013**: `InvalidMerchantTreasuryAccount` - Invalid merchant treasury account
12//! - **6014**: `InvalidPlatformTreasuryAccount` - Invalid platform treasury account
13//! - **6015**: `InvalidUsdcMint` - Invalid USDC mint account
14//! - **6016**: `MerchantNotFound` - Merchant account not found or invalid
15//! - **6017**: `PlanNotFound` - Subscription plan not found or invalid
16//! - **6018**: `SubscriptionNotFound` - Subscription not found or invalid
17//! - **6019**: `ConfigNotFound` - Global configuration account not found
18//!
19//! # Example
20//!
21//! ```rust
22//! use tally_sdk::{SimpleTallyClient, error::TallyError};
23//! use anchor_lang::prelude::Pubkey;
24//!
25//! async fn handle_transaction_error() {
26//!     let client = SimpleTallyClient::new("https://api.devnet.solana.com").unwrap();
27//!     let some_address = Pubkey::default();
28//!
29//!     // When a transaction fails, you get specific error information:
30//!     match client.get_merchant(&some_address) {
31//!         Ok(merchant) => println!("Found merchant: {:?}", merchant),
32//!         Err(TallyError::MerchantNotFound) => {
33//!             println!("Merchant account not found - ensure it's properly initialized");
34//!         }
35//!         Err(TallyError::InvalidSubscriberTokenAccount) => {
36//!             println!("Invalid subscriber token account provided");
37//!         }
38//!         Err(other_error) => {
39//!             println!("Other error: {}", other_error);
40//!         }
41//!     }
42//! }
43//! ```
44
45use thiserror::Error;
46
47/// Result type for Tally SDK operations
48pub type Result<T> = std::result::Result<T, TallyError>;
49
50/// Error types that can occur when using the Tally SDK
51#[derive(Error, Debug)]
52pub enum TallyError {
53    /// Error from Anchor framework
54    #[error("Anchor error: {0}")]
55    Anchor(anchor_lang::error::Error),
56
57    /// Error from Anchor client
58    #[error("Anchor client error: {0}")]
59    AnchorClient(Box<anchor_client::ClientError>),
60
61    /// Error from Solana SDK
62    #[error("Solana SDK error: {0}")]
63    Solana(#[from] anchor_client::solana_sdk::pubkey::ParsePubkeyError),
64
65    /// Error from SPL Token
66    #[error("SPL Token error: {0}")]
67    SplToken(#[from] spl_token::error::TokenError),
68
69    /// Error from Solana Program
70    #[error("Program error: {0}")]
71    Program(#[from] solana_program::program_error::ProgramError),
72
73    /// Error from serde JSON
74    #[error("JSON error: {0}")]
75    Json(#[from] serde_json::Error),
76
77    /// Generic error with message
78    #[error("Tally SDK error: {0}")]
79    Generic(String),
80
81    /// Event parsing error
82    #[error("Event parsing error: {0}")]
83    ParseError(String),
84
85    /// Invalid PDA computation
86    #[error("Invalid PDA: {0}")]
87    InvalidPda(String),
88
89    /// Invalid token program
90    #[error("Invalid token program: expected {expected}, found {found}")]
91    InvalidTokenProgram { expected: String, found: String },
92
93    /// Account not found
94    #[error("Account not found: {0}")]
95    AccountNotFound(String),
96
97    /// Insufficient funds
98    #[error("Insufficient funds: required {required}, available {available}")]
99    InsufficientFunds { required: u64, available: u64 },
100
101    /// Invalid subscription state
102    #[error("Invalid subscription state: {0}")]
103    InvalidSubscriptionState(String),
104
105    /// Token program detection failed
106    #[error("Failed to detect token program for mint: {mint}")]
107    TokenProgramDetectionFailed { mint: String },
108
109    /// RPC error for blockchain queries
110    #[error("RPC error: {0}")]
111    RpcError(String),
112
113    // Specific program error variants (maps to Anchor error codes 6012-6019)
114    /// Invalid subscriber token account (program error 6012)
115    #[error("Invalid subscriber token account. Ensure the account is a valid USDC token account owned by the subscriber.")]
116    InvalidSubscriberTokenAccount,
117
118    /// Invalid merchant treasury token account (program error 6013)
119    #[error("Invalid merchant treasury token account. Ensure the account is a valid USDC token account.")]
120    InvalidMerchantTreasuryAccount,
121
122    /// Invalid platform treasury token account (program error 6014)
123    #[error("Invalid platform treasury token account. Ensure the account is a valid USDC token account.")]
124    InvalidPlatformTreasuryAccount,
125
126    /// Invalid USDC mint account (program error 6015)
127    #[error("Invalid USDC mint account. Ensure the account is a valid token mint account.")]
128    InvalidUsdcMint,
129
130    /// Merchant account not found or invalid (program error 6016)
131    #[error(
132        "Merchant account not found or invalid. Ensure the merchant has been properly initialized."
133    )]
134    MerchantNotFound,
135
136    /// Subscription plan not found or invalid (program error 6017)
137    #[error("Subscription plan not found or invalid. Ensure the plan exists and belongs to the specified merchant.")]
138    PlanNotFound,
139
140    /// Subscription not found or invalid (program error 6018)
141    #[error("Subscription not found or invalid. Ensure the subscription exists for this plan and subscriber.")]
142    SubscriptionNotFound,
143
144    /// Global configuration account not found or invalid (program error 6019)
145    #[error("Global configuration account not found or invalid. Ensure the program has been properly initialized.")]
146    ConfigNotFound,
147}
148
149// Update the From implementation for anchor_client::ClientError to use our mapping
150impl From<anchor_client::ClientError> for TallyError {
151    fn from(error: anchor_client::ClientError) -> Self {
152        Self::from_anchor_client_error(error)
153    }
154}
155
156// Update the From implementation for anchor_lang::error::Error to use our mapping
157impl From<anchor_lang::error::Error> for TallyError {
158    fn from(error: anchor_lang::error::Error) -> Self {
159        Self::from_anchor_error(error)
160    }
161}
162
163impl From<String> for TallyError {
164    fn from(msg: String) -> Self {
165        Self::Generic(msg)
166    }
167}
168
169impl From<&str> for TallyError {
170    fn from(msg: &str) -> Self {
171        Self::Generic(msg.to_string())
172    }
173}
174
175impl From<anchor_lang::prelude::ProgramError> for TallyError {
176    fn from(error: anchor_lang::prelude::ProgramError) -> Self {
177        Self::Generic(format!("Program error: {error:?}"))
178    }
179}
180
181impl From<anyhow::Error> for TallyError {
182    fn from(error: anyhow::Error) -> Self {
183        Self::Generic(error.to_string())
184    }
185}
186
187impl TallyError {
188    /// Map program error codes to specific `TallyError` variants
189    ///
190    /// This function takes an Anchor error and attempts to map it to a more specific
191    /// `TallyError` variant based on the error code. If no specific mapping exists,
192    /// it returns the original Anchor error wrapped in `TallyError::Anchor`.
193    ///
194    /// # Arguments
195    /// * `anchor_error` - The Anchor error to map
196    ///
197    /// # Returns
198    /// * `TallyError` - The mapped specific error variant or generic Anchor error
199    #[must_use]
200    pub fn from_anchor_error(anchor_error: anchor_lang::error::Error) -> Self {
201        use anchor_lang::error::Error;
202
203        match &anchor_error {
204            Error::AnchorError(anchor_err) => {
205                // Map specific error codes to our custom variants
206                // Anchor assigns error codes starting from 6000 for custom errors
207                match anchor_err.error_code_number {
208                    6012 => Self::InvalidSubscriberTokenAccount,
209                    6013 => Self::InvalidMerchantTreasuryAccount,
210                    6014 => Self::InvalidPlatformTreasuryAccount,
211                    6015 => Self::InvalidUsdcMint,
212                    6016 => Self::MerchantNotFound,
213                    6017 => Self::PlanNotFound,
214                    6018 => Self::SubscriptionNotFound,
215                    6019 => Self::ConfigNotFound,
216                    // For any other error codes, fall back to the generic Anchor error
217                    _ => Self::Anchor(anchor_error),
218                }
219            }
220            // For non-AnchorError variants, use the generic Anchor wrapper
221            Error::ProgramError(_) => Self::Anchor(anchor_error),
222        }
223    }
224
225    /// Convenience method to map Anchor client errors to specific `TallyError` variants
226    ///
227    /// # Arguments
228    /// * `client_error` - The Anchor client error to map
229    ///
230    /// # Returns
231    /// * `TallyError` - The mapped specific error variant or generic client error
232    pub fn from_anchor_client_error(client_error: anchor_client::ClientError) -> Self {
233        // Check if the client error contains a program error we can map
234        if let anchor_client::ClientError::SolanaClientError(solana_err) = &client_error {
235            // Use get_transaction_error() method as suggested by the compiler
236            if let Some(
237                anchor_client::solana_sdk::transaction::TransactionError::InstructionError(
238                    _,
239                    anchor_client::solana_sdk::instruction::InstructionError::Custom(error_code),
240                ),
241            ) = solana_err.get_transaction_error()
242            {
243                // Map specific program error codes
244                match error_code {
245                    6012 => return Self::InvalidSubscriberTokenAccount,
246                    6013 => return Self::InvalidMerchantTreasuryAccount,
247                    6014 => return Self::InvalidPlatformTreasuryAccount,
248                    6015 => return Self::InvalidUsdcMint,
249                    6016 => return Self::MerchantNotFound,
250                    6017 => return Self::PlanNotFound,
251                    6018 => return Self::SubscriptionNotFound,
252                    6019 => return Self::ConfigNotFound,
253                    _ => {} // Fall through to generic handling
254                }
255            }
256        }
257
258        // If no specific mapping found, use the generic client error wrapper
259        Self::AnchorClient(Box::new(client_error))
260    }
261}