Skip to main content

near_kit/
error.rs

1//! Error types for near-kit.
2//!
3//! This module provides comprehensive error types for all near-kit operations.
4//!
5//! # Error Hierarchy
6//!
7//! - [`Error`](enum@Error) — Main error type, returned by most operations
8//!   - [`RpcError`] — RPC/network errors (connectivity, account not found, etc.)
9//!   - [`InvalidTxError`][crate::types::InvalidTxError] — Transaction was rejected
10//!     before execution (bad nonce, insufficient balance, expired, etc.)
11//!
12//! Action errors (contract panics, missing keys, etc.) are **not** `Err` — the
13//! transaction was accepted and executed, so the outcome is returned as
14//! `Ok(outcome)` where `outcome.is_failure()` is `true`.
15//!
16//! # Error Handling Examples
17//!
18//! ## Handling Transaction Errors
19//!
20//! ```rust,no_run
21//! use near_kit::*;
22//!
23//! # async fn example() -> Result<(), Error> {
24//! let near = Near::testnet().build();
25//!
26//! match near.transfer("bob.testnet", "1 NEAR").await {
27//!     Ok(outcome) if outcome.is_success() => {
28//!         println!("Success! Hash: {}", outcome.transaction_hash());
29//!     }
30//!     Ok(outcome) => {
31//!         // Transaction executed but an action failed — inspect the outcome
32//!         println!("Action failed: {:?}, gas used: {}", outcome.failure_message(), outcome.total_gas_used());
33//!     }
34//!     Err(Error::InvalidTx(e)) => {
35//!         // Transaction was rejected before execution (bad nonce, insufficient balance, etc.)
36//!         println!("Transaction rejected: {}", e);
37//!     }
38//!     Err(e) => return Err(e),
39//! }
40//! # Ok(())
41//! # }
42//! ```
43//!
44//! ## Checking Retryable Errors
45//!
46//! ```rust,no_run
47//! use near_kit::RpcError;
48//!
49//! fn should_retry(err: &RpcError) -> bool {
50//!     err.is_retryable()
51//! }
52//! ```
53
54use thiserror::Error;
55
56use crate::types::{AccountId, DelegateDecodeError, InvalidTxError, PublicKey};
57
58/// Error parsing an account ID.
59///
60/// This is a re-export of the upstream [`near_account_id::ParseAccountError`].
61pub type ParseAccountIdError = near_account_id::ParseAccountError;
62
63/// Error parsing a NEAR token amount.
64#[derive(Debug, Clone, Error, PartialEq, Eq)]
65pub enum ParseAmountError {
66    #[error("Ambiguous amount '{0}'. Use explicit units like '5 NEAR' or '1000 yocto'")]
67    AmbiguousAmount(String),
68
69    #[error("Invalid amount format: '{0}'")]
70    InvalidFormat(String),
71
72    #[error("Invalid number in amount: '{0}'")]
73    InvalidNumber(String),
74
75    #[error("Amount overflow: value too large")]
76    Overflow,
77}
78
79/// Error parsing a gas value.
80#[derive(Debug, Clone, Error, PartialEq, Eq)]
81pub enum ParseGasError {
82    #[error("Invalid gas format: '{0}'. Use '30 Tgas', '5 Ggas', or '1000000 gas'")]
83    InvalidFormat(String),
84
85    #[error("Invalid number in gas: '{0}'")]
86    InvalidNumber(String),
87
88    #[error("Gas overflow: value too large")]
89    Overflow,
90}
91
92/// Error parsing a public or secret key.
93#[derive(Debug, Clone, Error, PartialEq, Eq)]
94pub enum ParseKeyError {
95    #[error("Invalid key format: expected 'ed25519:...' or 'secp256k1:...'")]
96    InvalidFormat,
97
98    #[error("Unknown key type: '{0}'")]
99    UnknownKeyType(String),
100
101    #[error("Invalid base58 encoding: {0}")]
102    InvalidBase58(String),
103
104    #[error("Invalid key length: expected {expected} bytes, got {actual}")]
105    InvalidLength { expected: usize, actual: usize },
106
107    #[error("Invalid curve point: key bytes do not represent a valid point on the curve")]
108    InvalidCurvePoint,
109
110    #[error("Invalid scalar: secret key bytes are not a valid scalar for this curve")]
111    InvalidScalar,
112}
113
114/// Error parsing a crypto hash.
115#[derive(Debug, Clone, Error, PartialEq, Eq)]
116pub enum ParseHashError {
117    #[error("Invalid base58 encoding: {0}")]
118    InvalidBase58(String),
119
120    #[error("Invalid hash length: expected 32 bytes, got {0}")]
121    InvalidLength(usize),
122}
123
124/// Error during signing operations.
125#[derive(Debug, Clone, Error, PartialEq, Eq)]
126pub enum SignerError {
127    #[error("Invalid seed phrase")]
128    InvalidSeedPhrase,
129
130    #[error("Signing failed: {0}")]
131    SigningFailed(String),
132
133    #[error("Key derivation failed: {0}")]
134    KeyDerivationFailed(String),
135}
136
137/// Error during keystore operations.
138#[derive(Debug, Error)]
139pub enum KeyStoreError {
140    #[error("Key not found for account: {0}")]
141    KeyNotFound(AccountId),
142
143    #[error("IO error: {0}")]
144    Io(#[from] std::io::Error),
145
146    #[error("JSON error: {0}")]
147    Json(#[from] serde_json::Error),
148
149    #[error("Invalid credential format: {0}")]
150    InvalidFormat(String),
151
152    #[error("Invalid key: {0}")]
153    InvalidKey(#[from] ParseKeyError),
154
155    #[error("Path error: {0}")]
156    PathError(String),
157
158    #[error("Platform keyring error: {0}")]
159    Platform(String),
160}
161
162// ============================================================================
163// RPC Errors
164// ============================================================================
165
166/// RPC-specific errors.
167#[derive(Debug, Error)]
168pub enum RpcError {
169    // ─── Network/Transport ───
170    #[error("HTTP error: {0}")]
171    Http(#[from] reqwest::Error),
172
173    #[error("Network error: {message}")]
174    Network {
175        message: String,
176        status_code: Option<u16>,
177        retryable: bool,
178    },
179
180    #[error("Timeout after {0} retries")]
181    Timeout(u32),
182
183    #[error("JSON parse error: {0}")]
184    Json(#[from] serde_json::Error),
185
186    #[error("Invalid response: {0}")]
187    InvalidResponse(String),
188
189    // ─── Generic RPC Error ───
190    #[error("RPC error: {message} (code: {code})")]
191    Rpc {
192        code: i64,
193        message: String,
194        data: Option<serde_json::Value>,
195    },
196
197    // ─── Account Errors ───
198    #[error("Account not found: {0}")]
199    AccountNotFound(AccountId),
200
201    #[error("Invalid account ID: {0}")]
202    InvalidAccount(String),
203
204    #[error("Access key not found: {account_id} / {public_key}")]
205    AccessKeyNotFound {
206        account_id: AccountId,
207        public_key: PublicKey,
208    },
209
210    // ─── Contract Errors ───
211    #[error("Contract not deployed on account: {0}")]
212    ContractNotDeployed(AccountId),
213
214    #[error("Contract state too large for account: {0}")]
215    ContractStateTooLarge(AccountId),
216
217    #[error("Contract execution failed on {contract_id}: {message}")]
218    ContractExecution {
219        contract_id: AccountId,
220        method_name: Option<String>,
221        message: String,
222    },
223
224    #[error("Contract panic: {message}")]
225    ContractPanic { message: String },
226
227    #[error("Function call error on {contract_id}.{method_name}: {}", panic.as_deref().unwrap_or("unknown error"))]
228    FunctionCall {
229        contract_id: AccountId,
230        method_name: String,
231        panic: Option<String>,
232        logs: Vec<String>,
233    },
234
235    // ─── Block/Chunk Errors ───
236    #[error(
237        "Block not found: {0}. It may have been garbage-collected. Try an archival node for blocks older than 5 epochs."
238    )]
239    UnknownBlock(String),
240
241    #[error("Chunk not found: {0}. It may have been garbage-collected. Try an archival node.")]
242    UnknownChunk(String),
243
244    #[error(
245        "Epoch not found for block: {0}. The block may be invalid or too old. Try an archival node."
246    )]
247    UnknownEpoch(String),
248
249    #[error("Invalid shard ID: {0}")]
250    InvalidShardId(String),
251
252    // ─── Receipt Errors ───
253    #[error("Receipt not found: {0}")]
254    UnknownReceipt(String),
255
256    // ─── Transaction Errors ───
257    /// Structured transaction validation error, parsed from nearcore's
258    /// `TxExecutionError::InvalidTxError`. Prefer matching on
259    /// [`Error::InvalidTx`] instead — this variant only appears when using
260    /// the low-level `rpc().send_tx()` API directly.
261    #[error("Invalid transaction: {0}")]
262    InvalidTx(crate::types::InvalidTxError),
263
264    /// Fallback when the RPC returns `INVALID_TRANSACTION` but the structured
265    /// error could not be deserialized into [`InvalidTxError`][crate::types::InvalidTxError].
266    #[error("Invalid transaction: {message}")]
267    InvalidTransaction {
268        message: String,
269        details: Option<serde_json::Value>,
270    },
271
272    // ─── Node Errors ───
273    #[error("Shard unavailable: {0}")]
274    ShardUnavailable(String),
275
276    #[error("Node not synced: {0}")]
277    NodeNotSynced(String),
278
279    #[error("Internal server error: {0}")]
280    InternalError(String),
281
282    // ─── Request Errors ───
283    #[error("Parse error: {0}")]
284    ParseError(String),
285
286    #[error("Request timeout: {message}")]
287    RequestTimeout {
288        message: String,
289        transaction_hash: Option<String>,
290    },
291}
292
293impl RpcError {
294    /// Check if this error is retryable.
295    pub fn is_retryable(&self) -> bool {
296        match self {
297            RpcError::Http(e) => e.is_timeout() || e.is_connect(),
298            RpcError::Timeout(_) => true,
299            RpcError::Network { retryable, .. } => *retryable,
300            RpcError::ShardUnavailable(_) => true,
301            RpcError::NodeNotSynced(_) => true,
302            RpcError::InternalError(_) => true,
303            RpcError::RequestTimeout { .. } => true,
304            RpcError::InvalidTx(e) => e.is_retryable(),
305            RpcError::Rpc { code, .. } => {
306                // Retry on server errors
307                *code == -32000 || *code == -32603
308            }
309            _ => false,
310        }
311    }
312
313    /// Create a network error.
314    pub fn network(message: impl Into<String>, status_code: Option<u16>, retryable: bool) -> Self {
315        RpcError::Network {
316            message: message.into(),
317            status_code,
318            retryable,
319        }
320    }
321
322    /// Create an invalid transaction error, attempting to parse structured data first.
323    pub fn invalid_transaction(
324        message: impl Into<String>,
325        details: Option<serde_json::Value>,
326    ) -> Self {
327        // Try to deserialize the structured error from the data field
328        if let Some(ref data) = details {
329            if let Some(invalid_tx) = Self::try_parse_invalid_tx(data) {
330                return RpcError::InvalidTx(invalid_tx);
331            }
332        }
333
334        RpcError::InvalidTransaction {
335            message: message.into(),
336            details,
337        }
338    }
339
340    /// Try to extract an `InvalidTxError` from the RPC error data JSON.
341    ///
342    /// The data field typically looks like:
343    /// `{"TxExecutionError": {"InvalidTxError": {"InvalidNonce": {...}}}}`
344    fn try_parse_invalid_tx(data: &serde_json::Value) -> Option<crate::types::InvalidTxError> {
345        // Nested form: data.TxExecutionError.InvalidTxError
346        if let Some(tx_err) = data.get("TxExecutionError") {
347            if let Some(invalid_tx_value) = tx_err.get("InvalidTxError") {
348                if let Ok(parsed) = serde_json::from_value(invalid_tx_value.clone()) {
349                    return Some(parsed);
350                }
351            }
352        }
353
354        // Fallback: InvalidTxError at the top level (some RPC versions)
355        if let Some(invalid_tx_value) = data.get("InvalidTxError") {
356            if let Ok(parsed) = serde_json::from_value(invalid_tx_value.clone()) {
357                return Some(parsed);
358            }
359        }
360
361        None
362    }
363
364    /// Create a function call error.
365    pub fn function_call(
366        contract_id: AccountId,
367        method_name: impl Into<String>,
368        panic: Option<String>,
369        logs: Vec<String>,
370    ) -> Self {
371        RpcError::FunctionCall {
372            contract_id,
373            method_name: method_name.into(),
374            panic,
375            logs,
376        }
377    }
378}
379
380// ============================================================================
381// Main Error Type
382// ============================================================================
383
384/// Main error type for near-kit operations.
385impl RpcError {
386    /// Returns true if this error indicates the account was not found.
387    pub fn is_account_not_found(&self) -> bool {
388        matches!(self, RpcError::AccountNotFound(_))
389    }
390
391    /// Returns true if this error indicates a contract is not deployed.
392    pub fn is_contract_not_deployed(&self) -> bool {
393        matches!(self, RpcError::ContractNotDeployed(_))
394    }
395}
396
397#[derive(Debug, Error)]
398pub enum Error {
399    // ─── Configuration ───
400    #[error(
401        "No signer configured. Use .credentials()/.signer() on NearBuilder, .with_signer() on the client, or .sign_with() on the transaction."
402    )]
403    NoSigner,
404
405    #[error(
406        "No signer account ID. Call .default_account() on NearBuilder or use a signer with an account ID."
407    )]
408    NoSignerAccount,
409
410    #[error("Invalid configuration: {0}")]
411    Config(String),
412
413    // ─── Parsing ───
414    #[error(transparent)]
415    ParseAccountId(#[from] ParseAccountIdError),
416
417    #[error(transparent)]
418    ParseAmount(#[from] ParseAmountError),
419
420    #[error(transparent)]
421    ParseGas(#[from] ParseGasError),
422
423    #[error(transparent)]
424    ParseKey(#[from] ParseKeyError),
425
426    // ─── RPC ───
427    #[error(transparent)]
428    Rpc(Box<RpcError>),
429
430    // ─── Transaction validation ───
431    /// Transaction was rejected before execution. No receipt was created,
432    /// no nonce was incremented, no gas was consumed.
433    ///
434    /// This covers validation failures from both the RPC pre-check and the
435    /// runtime execution layer — the caller never needs to distinguish them.
436    #[error("Invalid transaction: {0}")]
437    InvalidTx(Box<InvalidTxError>),
438
439    /// Local pre-send validation failure (e.g. empty actions, bad
440    /// deserialization). Not a nearcore error.
441    #[error("Invalid transaction: {0}")]
442    InvalidTransaction(String),
443
444    // ─── Signing ───
445    #[error("Signing failed: {0}")]
446    Signing(#[from] SignerError),
447
448    // ─── KeyStore ───
449    #[error(transparent)]
450    KeyStore(#[from] KeyStoreError),
451
452    // ─── Serialization ───
453    #[error("JSON error: {0}")]
454    Json(#[from] serde_json::Error),
455
456    #[error("Borsh error: {0}")]
457    Borsh(String),
458
459    #[error("Delegate action decode error: {0}")]
460    DelegateDecode(#[from] DelegateDecodeError),
461
462    // ─── Tokens ───
463    #[error("Token {token} is not available on chain {chain_id}")]
464    TokenNotAvailable { token: String, chain_id: String },
465}
466
467impl From<RpcError> for Error {
468    fn from(err: RpcError) -> Self {
469        match err {
470            // Promote structured tx validation errors to Error::InvalidTx
471            RpcError::InvalidTx(e) => Error::InvalidTx(Box::new(e)),
472            other => Error::Rpc(Box::new(other)),
473        }
474    }
475}
476
477impl Error {
478    /// Returns `true` if this is an [`Error::InvalidTx`] variant.
479    pub fn is_invalid_tx(&self) -> bool {
480        matches!(self, Error::InvalidTx(_))
481    }
482}
483
484#[cfg(test)]
485mod tests {
486    use super::*;
487
488    // ========================================================================
489    // ParseAccountIdError tests
490    // ========================================================================
491
492    #[test]
493    fn test_parse_account_id_error_display() {
494        // Upstream ParseAccountError has different variants.
495        // Test via parsing invalid strings.
496        let err = "".parse::<AccountId>().unwrap_err();
497        assert!(err.to_string().contains("too short"));
498
499        let err = "a".parse::<AccountId>().unwrap_err();
500        assert!(err.to_string().contains("too short"));
501
502        let err = "A.near".parse::<AccountId>().unwrap_err();
503        assert!(err.to_string().contains("invalid character"));
504
505        let err = "bad..account".parse::<AccountId>().unwrap_err();
506        assert!(err.to_string().contains("redundant separator"));
507    }
508
509    // ========================================================================
510    // ParseAmountError tests
511    // ========================================================================
512
513    #[test]
514    fn test_parse_amount_error_display() {
515        assert_eq!(
516            ParseAmountError::AmbiguousAmount("123".to_string()).to_string(),
517            "Ambiguous amount '123'. Use explicit units like '5 NEAR' or '1000 yocto'"
518        );
519        assert_eq!(
520            ParseAmountError::InvalidFormat("xyz".to_string()).to_string(),
521            "Invalid amount format: 'xyz'"
522        );
523        assert_eq!(
524            ParseAmountError::InvalidNumber("abc".to_string()).to_string(),
525            "Invalid number in amount: 'abc'"
526        );
527        assert_eq!(
528            ParseAmountError::Overflow.to_string(),
529            "Amount overflow: value too large"
530        );
531    }
532
533    // ========================================================================
534    // ParseGasError tests
535    // ========================================================================
536
537    #[test]
538    fn test_parse_gas_error_display() {
539        assert_eq!(
540            ParseGasError::InvalidFormat("xyz".to_string()).to_string(),
541            "Invalid gas format: 'xyz'. Use '30 Tgas', '5 Ggas', or '1000000 gas'"
542        );
543        assert_eq!(
544            ParseGasError::InvalidNumber("abc".to_string()).to_string(),
545            "Invalid number in gas: 'abc'"
546        );
547        assert_eq!(
548            ParseGasError::Overflow.to_string(),
549            "Gas overflow: value too large"
550        );
551    }
552
553    // ========================================================================
554    // ParseKeyError tests
555    // ========================================================================
556
557    #[test]
558    fn test_parse_key_error_display() {
559        assert_eq!(
560            ParseKeyError::InvalidFormat.to_string(),
561            "Invalid key format: expected 'ed25519:...' or 'secp256k1:...'"
562        );
563        assert_eq!(
564            ParseKeyError::UnknownKeyType("rsa".to_string()).to_string(),
565            "Unknown key type: 'rsa'"
566        );
567        assert_eq!(
568            ParseKeyError::InvalidBase58("invalid chars".to_string()).to_string(),
569            "Invalid base58 encoding: invalid chars"
570        );
571        assert_eq!(
572            ParseKeyError::InvalidLength {
573                expected: 32,
574                actual: 16
575            }
576            .to_string(),
577            "Invalid key length: expected 32 bytes, got 16"
578        );
579        assert_eq!(
580            ParseKeyError::InvalidCurvePoint.to_string(),
581            "Invalid curve point: key bytes do not represent a valid point on the curve"
582        );
583    }
584
585    // ========================================================================
586    // ParseHashError tests
587    // ========================================================================
588
589    #[test]
590    fn test_parse_hash_error_display() {
591        assert_eq!(
592            ParseHashError::InvalidBase58("bad input".to_string()).to_string(),
593            "Invalid base58 encoding: bad input"
594        );
595        assert_eq!(
596            ParseHashError::InvalidLength(16).to_string(),
597            "Invalid hash length: expected 32 bytes, got 16"
598        );
599    }
600
601    // ========================================================================
602    // SignerError tests
603    // ========================================================================
604
605    #[test]
606    fn test_signer_error_display() {
607        assert_eq!(
608            SignerError::InvalidSeedPhrase.to_string(),
609            "Invalid seed phrase"
610        );
611        assert_eq!(
612            SignerError::SigningFailed("hardware failure".to_string()).to_string(),
613            "Signing failed: hardware failure"
614        );
615        assert_eq!(
616            SignerError::KeyDerivationFailed("path error".to_string()).to_string(),
617            "Key derivation failed: path error"
618        );
619    }
620
621    // ========================================================================
622    // KeyStoreError tests
623    // ========================================================================
624
625    #[test]
626    fn test_keystore_error_display() {
627        let account_id: AccountId = "alice.near".parse().unwrap();
628        assert_eq!(
629            KeyStoreError::KeyNotFound(account_id).to_string(),
630            "Key not found for account: alice.near"
631        );
632        assert_eq!(
633            KeyStoreError::InvalidFormat("missing field".to_string()).to_string(),
634            "Invalid credential format: missing field"
635        );
636        assert_eq!(
637            KeyStoreError::PathError("bad path".to_string()).to_string(),
638            "Path error: bad path"
639        );
640        assert_eq!(
641            KeyStoreError::Platform("keyring locked".to_string()).to_string(),
642            "Platform keyring error: keyring locked"
643        );
644    }
645
646    // ========================================================================
647    // RpcError tests
648    // ========================================================================
649
650    #[test]
651    fn test_rpc_error_display() {
652        let account_id: AccountId = "alice.near".parse().unwrap();
653
654        assert_eq!(RpcError::Timeout(3).to_string(), "Timeout after 3 retries");
655        assert_eq!(
656            RpcError::InvalidResponse("missing result".to_string()).to_string(),
657            "Invalid response: missing result"
658        );
659        assert_eq!(
660            RpcError::AccountNotFound(account_id.clone()).to_string(),
661            "Account not found: alice.near"
662        );
663        assert_eq!(
664            RpcError::InvalidAccount("bad-account".to_string()).to_string(),
665            "Invalid account ID: bad-account"
666        );
667        assert_eq!(
668            RpcError::ContractNotDeployed(account_id.clone()).to_string(),
669            "Contract not deployed on account: alice.near"
670        );
671        assert_eq!(
672            RpcError::ContractStateTooLarge(account_id.clone()).to_string(),
673            "Contract state too large for account: alice.near"
674        );
675        assert_eq!(
676            RpcError::UnknownBlock("12345".to_string()).to_string(),
677            "Block not found: 12345. It may have been garbage-collected. Try an archival node for blocks older than 5 epochs."
678        );
679        assert_eq!(
680            RpcError::UnknownChunk("abc123".to_string()).to_string(),
681            "Chunk not found: abc123. It may have been garbage-collected. Try an archival node."
682        );
683        assert_eq!(
684            RpcError::UnknownEpoch("epoch1".to_string()).to_string(),
685            "Epoch not found for block: epoch1. The block may be invalid or too old. Try an archival node."
686        );
687        assert_eq!(
688            RpcError::UnknownReceipt("receipt123".to_string()).to_string(),
689            "Receipt not found: receipt123"
690        );
691        assert_eq!(
692            RpcError::InvalidShardId("99".to_string()).to_string(),
693            "Invalid shard ID: 99"
694        );
695        assert_eq!(
696            RpcError::ShardUnavailable("shard 0".to_string()).to_string(),
697            "Shard unavailable: shard 0"
698        );
699        assert_eq!(
700            RpcError::NodeNotSynced("syncing...".to_string()).to_string(),
701            "Node not synced: syncing..."
702        );
703        assert_eq!(
704            RpcError::InternalError("database error".to_string()).to_string(),
705            "Internal server error: database error"
706        );
707        assert_eq!(
708            RpcError::ParseError("invalid json".to_string()).to_string(),
709            "Parse error: invalid json"
710        );
711    }
712
713    #[test]
714    fn test_rpc_error_is_retryable() {
715        use crate::types::InvalidTxError;
716
717        // Retryable errors
718        assert!(RpcError::Timeout(3).is_retryable());
719        assert!(RpcError::ShardUnavailable("shard 0".to_string()).is_retryable());
720        assert!(RpcError::NodeNotSynced("syncing".to_string()).is_retryable());
721        assert!(RpcError::InternalError("db error".to_string()).is_retryable());
722        assert!(
723            RpcError::RequestTimeout {
724                message: "timeout".to_string(),
725                transaction_hash: None,
726            }
727            .is_retryable()
728        );
729        assert!(
730            RpcError::InvalidTx(InvalidTxError::InvalidNonce {
731                tx_nonce: 5,
732                ak_nonce: 10
733            })
734            .is_retryable()
735        );
736        assert!(
737            RpcError::InvalidTx(InvalidTxError::ShardCongested {
738                congestion_level: 1.0,
739                shard_id: 0,
740            })
741            .is_retryable()
742        );
743        assert!(
744            RpcError::Network {
745                message: "connection reset".to_string(),
746                status_code: Some(503),
747                retryable: true,
748            }
749            .is_retryable()
750        );
751        assert!(
752            RpcError::Rpc {
753                code: -32000,
754                message: "server error".to_string(),
755                data: None,
756            }
757            .is_retryable()
758        );
759
760        // Non-retryable errors
761        let account_id: AccountId = "alice.near".parse().unwrap();
762        assert!(!RpcError::AccountNotFound(account_id.clone()).is_retryable());
763        assert!(!RpcError::ContractNotDeployed(account_id.clone()).is_retryable());
764        assert!(!RpcError::InvalidAccount("bad".to_string()).is_retryable());
765        assert!(!RpcError::UnknownBlock("12345".to_string()).is_retryable());
766        assert!(!RpcError::ParseError("bad json".to_string()).is_retryable());
767        assert!(
768            !RpcError::InvalidTx(InvalidTxError::NotEnoughBalance {
769                signer_id: account_id.clone(),
770                balance: crate::types::NearToken::from_near(1),
771                cost: crate::types::NearToken::from_near(100),
772            })
773            .is_retryable()
774        );
775        assert!(
776            !RpcError::InvalidTransaction {
777                message: "invalid".to_string(),
778                details: None,
779            }
780            .is_retryable()
781        );
782    }
783
784    #[test]
785    fn test_rpc_error_network_constructor() {
786        let err = RpcError::network("connection refused", Some(503), true);
787        match err {
788            RpcError::Network {
789                message,
790                status_code,
791                retryable,
792            } => {
793                assert_eq!(message, "connection refused");
794                assert_eq!(status_code, Some(503));
795                assert!(retryable);
796            }
797            _ => panic!("Expected Network error"),
798        }
799    }
800
801    #[test]
802    fn test_rpc_error_invalid_transaction_constructor_unstructured() {
803        let err = RpcError::invalid_transaction("invalid nonce", None);
804        match err {
805            RpcError::InvalidTransaction { message, details } => {
806                assert_eq!(message, "invalid nonce");
807                assert!(details.is_none());
808            }
809            _ => panic!("Expected InvalidTransaction error"),
810        }
811    }
812
813    #[test]
814    fn test_rpc_error_invalid_transaction_constructor_structured() {
815        // When data contains a parseable InvalidTxError, it should produce InvalidTx
816        let data = serde_json::json!({
817            "TxExecutionError": {
818                "InvalidTxError": {
819                    "InvalidNonce": {
820                        "tx_nonce": 5,
821                        "ak_nonce": 10
822                    }
823                }
824            }
825        });
826        let err = RpcError::invalid_transaction("invalid nonce", Some(data));
827        match err {
828            RpcError::InvalidTx(crate::types::InvalidTxError::InvalidNonce {
829                tx_nonce,
830                ak_nonce,
831            }) => {
832                assert_eq!(tx_nonce, 5);
833                assert_eq!(ak_nonce, 10);
834            }
835            other => panic!("Expected InvalidTx(InvalidNonce), got: {other:?}"),
836        }
837    }
838
839    #[test]
840    fn test_rpc_error_function_call_constructor() {
841        let account_id: AccountId = "contract.near".parse().unwrap();
842        let err = RpcError::function_call(
843            account_id.clone(),
844            "my_method",
845            Some("assertion failed".to_string()),
846            vec!["log1".to_string(), "log2".to_string()],
847        );
848        match err {
849            RpcError::FunctionCall {
850                contract_id,
851                method_name,
852                panic,
853                logs,
854            } => {
855                assert_eq!(contract_id, account_id);
856                assert_eq!(method_name, "my_method");
857                assert_eq!(panic, Some("assertion failed".to_string()));
858                assert_eq!(logs, vec!["log1", "log2"]);
859            }
860            _ => panic!("Expected FunctionCall error"),
861        }
862    }
863
864    #[test]
865    fn test_rpc_error_is_account_not_found() {
866        let account_id: AccountId = "alice.near".parse().unwrap();
867        assert!(RpcError::AccountNotFound(account_id).is_account_not_found());
868        assert!(!RpcError::Timeout(3).is_account_not_found());
869    }
870
871    #[test]
872    fn test_rpc_error_is_contract_not_deployed() {
873        let account_id: AccountId = "alice.near".parse().unwrap();
874        assert!(RpcError::ContractNotDeployed(account_id).is_contract_not_deployed());
875        assert!(!RpcError::Timeout(3).is_contract_not_deployed());
876    }
877
878    #[test]
879    fn test_rpc_error_contract_execution_display() {
880        let account_id: AccountId = "contract.near".parse().unwrap();
881        let err = RpcError::ContractExecution {
882            contract_id: account_id,
883            method_name: Some("my_method".to_string()),
884            message: "execution failed".to_string(),
885        };
886        assert_eq!(
887            err.to_string(),
888            "Contract execution failed on contract.near: execution failed"
889        );
890    }
891
892    #[test]
893    fn test_rpc_error_function_call_display() {
894        let account_id: AccountId = "contract.near".parse().unwrap();
895        let err = RpcError::FunctionCall {
896            contract_id: account_id.clone(),
897            method_name: "my_method".to_string(),
898            panic: Some("assertion failed".to_string()),
899            logs: vec![],
900        };
901        assert_eq!(
902            err.to_string(),
903            "Function call error on contract.near.my_method: assertion failed"
904        );
905
906        let err_no_panic = RpcError::FunctionCall {
907            contract_id: account_id,
908            method_name: "other_method".to_string(),
909            panic: None,
910            logs: vec![],
911        };
912        assert_eq!(
913            err_no_panic.to_string(),
914            "Function call error on contract.near.other_method: unknown error"
915        );
916    }
917
918    #[test]
919    fn test_rpc_error_invalid_tx_display() {
920        use crate::types::InvalidTxError;
921        let err = RpcError::InvalidTx(InvalidTxError::InvalidNonce {
922            tx_nonce: 5,
923            ak_nonce: 10,
924        });
925        assert!(err.to_string().contains("invalid nonce"));
926    }
927
928    #[test]
929    fn test_rpc_error_access_key_not_found_display() {
930        let account_id: AccountId = "alice.near".parse().unwrap();
931        let public_key: PublicKey = "ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp"
932            .parse()
933            .unwrap();
934        let err = RpcError::AccessKeyNotFound {
935            account_id,
936            public_key: public_key.clone(),
937        };
938        assert!(err.to_string().contains("alice.near"));
939        assert!(err.to_string().contains(&public_key.to_string()));
940    }
941
942    #[test]
943    fn test_rpc_error_request_timeout_display() {
944        let err = RpcError::RequestTimeout {
945            message: "request timed out".to_string(),
946            transaction_hash: Some("abc123".to_string()),
947        };
948        assert_eq!(err.to_string(), "Request timeout: request timed out");
949    }
950
951    // ========================================================================
952    // Error (main type) tests
953    // ========================================================================
954
955    #[test]
956    fn test_error_no_signer_display() {
957        assert_eq!(
958            Error::NoSigner.to_string(),
959            "No signer configured. Use .credentials()/.signer() on NearBuilder, .with_signer() on the client, or .sign_with() on the transaction."
960        );
961    }
962
963    #[test]
964    fn test_error_no_signer_account_display() {
965        assert_eq!(
966            Error::NoSignerAccount.to_string(),
967            "No signer account ID. Call .default_account() on NearBuilder or use a signer with an account ID."
968        );
969    }
970
971    #[test]
972    fn test_error_config_display() {
973        assert_eq!(
974            Error::Config("invalid url".to_string()).to_string(),
975            "Invalid configuration: invalid url"
976        );
977    }
978
979    #[test]
980    fn test_error_invalid_tx_display() {
981        use crate::types::InvalidTxError;
982        let err = Error::InvalidTx(Box::new(InvalidTxError::Expired));
983        assert!(err.to_string().contains("expired"));
984    }
985
986    #[test]
987    fn test_error_from_rpc_invalid_tx_promotes() {
988        use crate::types::InvalidTxError;
989        // RpcError::InvalidTx should become Error::InvalidTx, not Error::Rpc
990        let rpc_err = RpcError::InvalidTx(InvalidTxError::InvalidNonce {
991            tx_nonce: 5,
992            ak_nonce: 10,
993        });
994        let err: Error = rpc_err.into();
995        assert!(matches!(
996            err,
997            Error::InvalidTx(e) if matches!(*e, InvalidTxError::InvalidNonce { .. })
998        ));
999    }
1000
1001    #[test]
1002    fn test_error_borsh_display() {
1003        assert_eq!(
1004            Error::Borsh("deserialization failed".to_string()).to_string(),
1005            "Borsh error: deserialization failed"
1006        );
1007    }
1008
1009    #[test]
1010    fn test_error_from_parse_errors() {
1011        // ParseAccountIdError -> Error
1012        let parse_err = "".parse::<AccountId>().unwrap_err();
1013        let err: Error = parse_err.into();
1014        assert!(matches!(err, Error::ParseAccountId(_)));
1015
1016        // ParseAmountError -> Error
1017        let parse_err = ParseAmountError::Overflow;
1018        let err: Error = parse_err.into();
1019        assert!(matches!(err, Error::ParseAmount(_)));
1020
1021        // ParseGasError -> Error
1022        let parse_err = ParseGasError::Overflow;
1023        let err: Error = parse_err.into();
1024        assert!(matches!(err, Error::ParseGas(_)));
1025
1026        // ParseKeyError -> Error
1027        let parse_err = ParseKeyError::InvalidFormat;
1028        let err: Error = parse_err.into();
1029        assert!(matches!(err, Error::ParseKey(_)));
1030    }
1031
1032    #[test]
1033    fn test_error_from_rpc_error() {
1034        let rpc_err = RpcError::Timeout(3);
1035        let err: Error = rpc_err.into();
1036        assert!(matches!(err, Error::Rpc(_)));
1037    }
1038
1039    #[test]
1040    fn test_error_from_signer_error() {
1041        let signer_err = SignerError::InvalidSeedPhrase;
1042        let err: Error = signer_err.into();
1043        assert!(matches!(err, Error::Signing(_)));
1044    }
1045}