1use thiserror::Error;
4
5pub type Result<T> = std::result::Result<T, Error>;
7
8#[derive(Error, Debug)]
10pub enum Error {
11 #[error("Configuration error: {0}\n\nTip: {1}")]
13 Config(String, String),
14
15 #[error("Connection error: {0}\n\nTip: {1}")]
17 Connection(String, String),
18
19 #[error("Transaction error: {0}\n\nTip: {1}")]
21 Transaction(String, String),
22
23 #[error("Chain not supported: {0}\n\nSupported chains: Polkadot, Kusama, Westend (Substrate) | Ethereum, BSC, Polygon (EVM)\nUse `apex chain list` to see all supported chains")]
25 UnsupportedChain(String),
26
27 #[error("Invalid address format: {0}\n\nExpected formats:\n -Substrate (SS58): 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY\n -Ethereum (Hex): 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb")]
29 InvalidAddress(String),
30
31 #[error("Substrate adapter error: {0}")]
33 Substrate(#[from] apex_sdk_substrate::Error),
34
35 #[error("EVM adapter error: {0}")]
37 Evm(#[from] apex_sdk_evm::Error),
38
39 #[error("Serialization error: {0}")]
41 Serialization(String),
42
43 #[error("{0}")]
45 Other(String),
46}
47
48impl Error {
49 pub fn config(msg: impl Into<String>) -> Self {
51 let msg_str = msg.into();
52 let tip = if msg_str.contains("adapter") || msg_str.contains("configured") {
53 "Use .with_substrate() or .with_evm() when building the SDK:\n \
54 ApexSDK::builder().with_substrate(Chain::Polkadot, \"wss://rpc.polkadot.io\").build()?"
55 } else {
56 "Check your configuration and ensure all required fields are set"
57 };
58 Error::Config(msg_str, tip.to_string())
59 }
60
61 pub fn connection(msg: impl Into<String>) -> Self {
63 let msg_str = msg.into();
64 let tip = if msg_str.contains("timeout") || msg_str.contains("timed out") {
65 "The endpoint may be slow or unreachable. Try:\n \
66 1. Use a different RPC endpoint\n \
67 2. Increase timeout with .with_timeout(Duration::from_secs(30))\n \
68 3. Check your internet connection"
69 } else if msg_str.contains("refused") || msg_str.contains("failed to connect") {
70 "Cannot reach the endpoint. Verify:\n \
71 1. The endpoint URL is correct\n \
72 2. Your firewall allows outbound connections\n \
73 3. The node is online and accessible"
74 } else {
75 "Check the endpoint URL and network connection"
76 };
77 Error::Connection(msg_str, tip.to_string())
78 }
79
80 pub fn transaction(msg: impl Into<String>) -> Self {
82 let msg_str = msg.into();
83 let tip = if msg_str.contains("insufficient") || msg_str.contains("balance") {
84 "Your account doesn't have enough balance. Ensure:\n \
85 1. The account has sufficient funds for transaction + fees\n \
86 2. You're using the correct account\n \
87 3. The chain is the right one for your tokens"
88 } else if msg_str.contains("nonce") {
89 "Transaction nonce error. Try:\n \
90 1. Wait for pending transactions to complete\n \
91 2. Use .with_nonce() to specify nonce manually\n \
92 3. Check for stuck transactions"
93 } else {
94 "Review the transaction parameters and account state"
95 };
96 Error::Transaction(msg_str, tip.to_string())
97 }
98}
99
100impl From<anyhow::Error> for Error {
101 fn from(err: anyhow::Error) -> Self {
102 Error::Other(err.to_string())
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn test_config_error_display() {
112 let error = Error::config("test config error");
113 assert!(error
114 .to_string()
115 .contains("Configuration error: test config error"));
116 assert!(error.to_string().contains("Tip:"));
117 }
118
119 #[test]
120 fn test_connection_error_display() {
121 let error = Error::connection("failed to connect");
122 assert!(error
123 .to_string()
124 .contains("Connection error: failed to connect"));
125 assert!(error.to_string().contains("Tip:"));
126 }
127
128 #[test]
129 fn test_transaction_error_display() {
130 let error = Error::transaction("invalid transaction");
131 assert!(error
132 .to_string()
133 .contains("Transaction error: invalid transaction"));
134 assert!(error.to_string().contains("Tip:"));
135 }
136
137 #[test]
138 fn test_unsupported_chain_error_display() {
139 let error = Error::UnsupportedChain("Unknown".to_string());
140 assert!(error.to_string().contains("Chain not supported: Unknown"));
141 assert!(error.to_string().contains("Supported chains:"));
142 }
143
144 #[test]
145 fn test_invalid_address_error_display() {
146 let error = Error::InvalidAddress("0xinvalid".to_string());
147 assert!(error
148 .to_string()
149 .contains("Invalid address format: 0xinvalid"));
150 assert!(error.to_string().contains("Expected formats:"));
151 }
152
153 #[test]
154 fn test_serialization_error_display() {
155 let error = Error::Serialization("JSON parse error".to_string());
156 assert_eq!(error.to_string(), "Serialization error: JSON parse error");
157 }
158
159 #[test]
160 fn test_other_error_display() {
161 let error = Error::Other("generic error".to_string());
162 assert_eq!(error.to_string(), "generic error");
163 }
164
165 #[test]
166 fn test_from_anyhow_error() {
167 let anyhow_err = anyhow::anyhow!("test error");
168 let error: Error = anyhow_err.into();
169 assert!(matches!(error, Error::Other(_)));
170 assert_eq!(error.to_string(), "test error");
171 }
172
173 #[test]
174 fn test_error_is_send_sync() {
175 fn assert_send<T: Send>() {}
176 fn assert_sync<T: Sync>() {}
177 assert_send::<Error>();
178 assert_sync::<Error>();
179 }
180}