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}