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