Skip to main content

near_kit/types/
error.rs

1//! Typed error types for NEAR transaction execution.
2//!
3//! These types mirror the error hierarchy returned by NEAR RPC in
4//! `ExecutionStatus::Failure`, replacing opaque `serde_json::Value`.
5
6use serde::Deserialize;
7
8use super::rpc::GlobalContractIdentifierView;
9use super::{AccountId, CryptoHash, Gas, NearToken, PublicKey};
10
11// ============================================================================
12// Top-level error
13// ============================================================================
14
15/// Error returned by NEAR RPC when a transaction or receipt fails.
16#[derive(Debug, Clone, Deserialize)]
17pub enum TxExecutionError {
18    /// An error happened during action execution.
19    ActionError(ActionError),
20    /// An error happened during transaction validation.
21    InvalidTxError(InvalidTxError),
22}
23
24impl std::fmt::Display for TxExecutionError {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        match self {
27            Self::ActionError(e) => write!(f, "ActionError: {e}"),
28            Self::InvalidTxError(e) => write!(f, "InvalidTxError: {e}"),
29        }
30    }
31}
32
33impl std::error::Error for TxExecutionError {}
34
35// ============================================================================
36// Action errors
37// ============================================================================
38
39/// An error that occurred during action execution.
40#[derive(Debug, Clone, Deserialize)]
41pub struct ActionError {
42    /// Index of the failed action in the transaction.
43    /// Not defined if kind is `ActionErrorKind::LackBalanceForState`.
44    #[serde(default)]
45    pub index: Option<u64>,
46    /// The kind of action error.
47    pub kind: ActionErrorKind,
48}
49
50impl std::fmt::Display for ActionError {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        match self.index {
53            Some(i) => write!(f, "action #{i}: {}", self.kind),
54            None => write!(f, "{}", self.kind),
55        }
56    }
57}
58
59/// Specific kind of action error.
60#[derive(Debug, Clone, Deserialize)]
61pub enum ActionErrorKind {
62    AccountAlreadyExists {
63        account_id: AccountId,
64    },
65    AccountDoesNotExist {
66        account_id: AccountId,
67    },
68    CreateAccountOnlyByRegistrar {
69        account_id: AccountId,
70        predecessor_id: AccountId,
71        registrar_account_id: AccountId,
72    },
73    CreateAccountNotAllowed {
74        account_id: AccountId,
75        predecessor_id: AccountId,
76    },
77    ActorNoPermission {
78        account_id: AccountId,
79        actor_id: AccountId,
80    },
81    DeleteKeyDoesNotExist {
82        account_id: AccountId,
83        public_key: PublicKey,
84    },
85    AddKeyAlreadyExists {
86        account_id: AccountId,
87        public_key: PublicKey,
88    },
89    DeleteAccountStaking {
90        account_id: AccountId,
91    },
92    LackBalanceForState {
93        account_id: AccountId,
94        amount: NearToken,
95    },
96    TriesToUnstake {
97        account_id: AccountId,
98    },
99    TriesToStake {
100        account_id: AccountId,
101        balance: NearToken,
102        locked: NearToken,
103        stake: NearToken,
104    },
105    InsufficientStake {
106        account_id: AccountId,
107        minimum_stake: NearToken,
108        stake: NearToken,
109    },
110    FunctionCallError(FunctionCallError),
111    NewReceiptValidationError(ReceiptValidationError),
112    OnlyImplicitAccountCreationAllowed {
113        account_id: AccountId,
114    },
115    DeleteAccountWithLargeState {
116        account_id: AccountId,
117    },
118    DelegateActionInvalidSignature,
119    DelegateActionSenderDoesNotMatchTxReceiver {
120        receiver_id: AccountId,
121        sender_id: AccountId,
122    },
123    DelegateActionExpired,
124    DelegateActionAccessKeyError(InvalidAccessKeyError),
125    DelegateActionInvalidNonce {
126        ak_nonce: u64,
127        delegate_nonce: u64,
128    },
129    DelegateActionNonceTooLarge {
130        delegate_nonce: u64,
131        upper_bound: u64,
132    },
133    GlobalContractDoesNotExist {
134        identifier: GlobalContractIdentifierView,
135    },
136    GasKeyDoesNotExist {
137        account_id: AccountId,
138        public_key: PublicKey,
139    },
140    InsufficientGasKeyBalance {
141        account_id: AccountId,
142        balance: NearToken,
143        public_key: PublicKey,
144        required: NearToken,
145    },
146    GasKeyBalanceTooHigh {
147        account_id: AccountId,
148        balance: NearToken,
149        #[serde(default)]
150        public_key: Option<PublicKey>,
151    },
152}
153
154impl std::fmt::Display for ActionErrorKind {
155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        match self {
157            Self::AccountAlreadyExists { account_id } => {
158                write!(f, "account {account_id} already exists")
159            }
160            Self::AccountDoesNotExist { account_id } => {
161                write!(f, "account {account_id} does not exist")
162            }
163            Self::LackBalanceForState { account_id, amount } => {
164                write!(f, "account {account_id} lacks {amount} for state")
165            }
166            Self::FunctionCallError(e) => write!(f, "{e}"),
167            _ => write!(f, "{self:?}"),
168        }
169    }
170}
171
172// ============================================================================
173// Transaction validation errors
174// ============================================================================
175
176/// An error during transaction validation (before execution).
177#[derive(Debug, Clone, Deserialize)]
178pub enum InvalidTxError {
179    InvalidAccessKeyError(InvalidAccessKeyError),
180    InvalidSignerId {
181        signer_id: String,
182    },
183    SignerDoesNotExist {
184        signer_id: AccountId,
185    },
186    InvalidNonce {
187        ak_nonce: u64,
188        tx_nonce: u64,
189    },
190    NonceTooLarge {
191        tx_nonce: u64,
192        upper_bound: u64,
193    },
194    InvalidReceiverId {
195        receiver_id: String,
196    },
197    InvalidSignature,
198    NotEnoughBalance {
199        balance: NearToken,
200        cost: NearToken,
201        signer_id: AccountId,
202    },
203    LackBalanceForState {
204        amount: NearToken,
205        signer_id: AccountId,
206    },
207    CostOverflow,
208    InvalidChain,
209    Expired,
210    ActionsValidation(ActionsValidationError),
211    TransactionSizeExceeded {
212        limit: u64,
213        size: u64,
214    },
215    InvalidTransactionVersion,
216    StorageError(StorageError),
217    ShardCongested {
218        congestion_level: f64,
219        shard_id: u64,
220    },
221    ShardStuck {
222        missed_chunks: u64,
223        shard_id: u64,
224    },
225    InvalidNonceIndex {
226        num_nonces: u16,
227        #[serde(default)]
228        tx_nonce_index: Option<u16>,
229    },
230    NotEnoughGasKeyBalance {
231        balance: NearToken,
232        cost: NearToken,
233        signer_id: AccountId,
234    },
235    NotEnoughBalanceForDeposit {
236        balance: NearToken,
237        cost: NearToken,
238        reason: DepositCostFailureReason,
239        signer_id: AccountId,
240    },
241}
242
243impl InvalidTxError {
244    /// Returns `true` if this error is transient and the transaction may
245    /// succeed on retry (e.g. invalid nonce, shard congestion).
246    pub fn is_retryable(&self) -> bool {
247        matches!(
248            self,
249            InvalidTxError::InvalidNonce { .. }
250                | InvalidTxError::ShardCongested { .. }
251                | InvalidTxError::ShardStuck { .. }
252        )
253    }
254}
255
256impl std::fmt::Display for InvalidTxError {
257    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258        match self {
259            Self::InvalidSignature => write!(f, "invalid signature"),
260            Self::NotEnoughBalance {
261                signer_id, cost, ..
262            } => write!(
263                f,
264                "{signer_id} does not have enough balance to cover {cost}"
265            ),
266            Self::InvalidNonce {
267                ak_nonce, tx_nonce, ..
268            } => write!(
269                f,
270                "invalid nonce: tx nonce {tx_nonce}, access key nonce {ak_nonce}"
271            ),
272            Self::Expired => write!(f, "transaction has expired"),
273            Self::ShardCongested { shard_id, .. } => write!(f, "shard {shard_id} is congested"),
274            _ => write!(f, "{self:?}"),
275        }
276    }
277}
278
279// ============================================================================
280// Access key errors
281// ============================================================================
282
283/// Error related to access key validation.
284#[derive(Debug, Clone, Deserialize)]
285pub enum InvalidAccessKeyError {
286    AccessKeyNotFound {
287        account_id: AccountId,
288        public_key: PublicKey,
289    },
290    ReceiverMismatch {
291        ak_receiver: String,
292        tx_receiver: AccountId,
293    },
294    MethodNameMismatch {
295        method_name: String,
296    },
297    RequiresFullAccess,
298    NotEnoughAllowance {
299        account_id: AccountId,
300        allowance: NearToken,
301        cost: NearToken,
302        public_key: PublicKey,
303    },
304    DepositWithFunctionCall,
305}
306
307// ============================================================================
308// Function call errors (VM / contract)
309// ============================================================================
310
311/// An error during function call execution.
312#[derive(Debug, Clone, Deserialize)]
313pub enum FunctionCallError {
314    WasmUnknownError,
315    #[serde(rename = "_EVMError")]
316    EvmError,
317    CompilationError(CompilationError),
318    LinkError {
319        msg: String,
320    },
321    MethodResolveError(MethodResolveError),
322    WasmTrap(WasmTrap),
323    HostError(HostError),
324    ExecutionError(String),
325}
326
327impl std::fmt::Display for FunctionCallError {
328    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
329        match self {
330            Self::ExecutionError(msg) => write!(f, "execution error: {msg}"),
331            Self::HostError(e) => write!(f, "host error: {e:?}"),
332            Self::WasmTrap(e) => write!(f, "wasm trap: {e:?}"),
333            Self::CompilationError(e) => write!(f, "compilation error: {e:?}"),
334            Self::MethodResolveError(e) => write!(f, "method resolve error: {e:?}"),
335            Self::LinkError { msg } => write!(f, "link error: {msg}"),
336            _ => write!(f, "{self:?}"),
337        }
338    }
339}
340
341/// Wasm compilation error.
342#[derive(Debug, Clone, Deserialize)]
343pub enum CompilationError {
344    CodeDoesNotExist { account_id: AccountId },
345    PrepareError(PrepareError),
346    WasmerCompileError { msg: String },
347}
348
349/// Error preparing a Wasm module.
350#[derive(Debug, Clone, Deserialize)]
351pub enum PrepareError {
352    Serialization,
353    Deserialization,
354    InternalMemoryDeclared,
355    GasInstrumentation,
356    StackHeightInstrumentation,
357    Instantiate,
358    Memory,
359    TooManyFunctions,
360    TooManyLocals,
361    TooManyTables,
362    TooManyTableElements,
363}
364
365/// Error resolving a method in Wasm.
366#[derive(Debug, Clone, Deserialize)]
367pub enum MethodResolveError {
368    MethodEmptyName,
369    MethodNotFound,
370    MethodInvalidSignature,
371}
372
373/// A trap during Wasm execution.
374#[derive(Debug, Clone, Deserialize)]
375pub enum WasmTrap {
376    Unreachable,
377    IncorrectCallIndirectSignature,
378    MemoryOutOfBounds,
379    #[serde(rename = "CallIndirectOOB")]
380    CallIndirectOob,
381    IllegalArithmetic,
382    MisalignedAtomicAccess,
383    IndirectCallToNull,
384    StackOverflow,
385    GenericTrap,
386}
387
388/// Error from a host function call.
389#[derive(Debug, Clone, Deserialize)]
390pub enum HostError {
391    #[serde(rename = "BadUTF16")]
392    BadUtf16,
393    #[serde(rename = "BadUTF8")]
394    BadUtf8,
395    GasExceeded,
396    GasLimitExceeded,
397    BalanceExceeded,
398    EmptyMethodName,
399    GuestPanic {
400        panic_msg: String,
401    },
402    IntegerOverflow,
403    InvalidPromiseIndex {
404        promise_idx: u64,
405    },
406    CannotAppendActionToJointPromise,
407    CannotReturnJointPromise,
408    InvalidPromiseResultIndex {
409        result_idx: u64,
410    },
411    InvalidRegisterId {
412        register_id: u64,
413    },
414    IteratorWasInvalidated {
415        iterator_index: u64,
416    },
417    MemoryAccessViolation,
418    InvalidReceiptIndex {
419        receipt_index: u64,
420    },
421    InvalidIteratorIndex {
422        iterator_index: u64,
423    },
424    InvalidAccountId,
425    InvalidMethodName,
426    InvalidPublicKey,
427    ProhibitedInView {
428        method_name: String,
429    },
430    NumberOfLogsExceeded {
431        limit: u64,
432    },
433    KeyLengthExceeded {
434        length: u64,
435        limit: u64,
436    },
437    ValueLengthExceeded {
438        length: u64,
439        limit: u64,
440    },
441    TotalLogLengthExceeded {
442        length: u64,
443        limit: u64,
444    },
445    NumberPromisesExceeded {
446        limit: u64,
447        number_of_promises: u64,
448    },
449    NumberInputDataDependenciesExceeded {
450        limit: u64,
451        number_of_input_data_dependencies: u64,
452    },
453    ReturnedValueLengthExceeded {
454        length: u64,
455        limit: u64,
456    },
457    ContractSizeExceeded {
458        limit: u64,
459        size: u64,
460    },
461    Deprecated {
462        method_name: String,
463    },
464    #[serde(rename = "ECRecoverError")]
465    EcRecoverError {
466        msg: String,
467    },
468    AltBn128InvalidInput {
469        msg: String,
470    },
471    Ed25519VerifyInvalidInput {
472        msg: String,
473    },
474}
475
476// ============================================================================
477// Actions validation errors
478// ============================================================================
479
480/// Error validating actions in a transaction or receipt.
481#[derive(Debug, Clone, Deserialize)]
482pub enum ActionsValidationError {
483    DeleteActionMustBeFinal,
484    TotalPrepaidGasExceeded {
485        limit: Gas,
486        total_prepaid_gas: Gas,
487    },
488    TotalNumberOfActionsExceeded {
489        limit: u64,
490        total_number_of_actions: u64,
491    },
492    AddKeyMethodNamesNumberOfBytesExceeded {
493        limit: u64,
494        total_number_of_bytes: u64,
495    },
496    AddKeyMethodNameLengthExceeded {
497        length: u64,
498        limit: u64,
499    },
500    IntegerOverflow,
501    InvalidAccountId {
502        account_id: String,
503    },
504    ContractSizeExceeded {
505        limit: u64,
506        size: u64,
507    },
508    FunctionCallMethodNameLengthExceeded {
509        length: u64,
510        limit: u64,
511    },
512    FunctionCallArgumentsLengthExceeded {
513        length: u64,
514        limit: u64,
515    },
516    UnsuitableStakingKey {
517        public_key: PublicKey,
518    },
519    FunctionCallZeroAttachedGas,
520    DelegateActionMustBeOnlyOne,
521    UnsupportedProtocolFeature {
522        protocol_feature: String,
523        version: u32,
524    },
525    InvalidDeterministicStateInitReceiver {
526        derived_id: AccountId,
527        receiver_id: AccountId,
528    },
529    DeterministicStateInitKeyLengthExceeded {
530        length: u64,
531        limit: u64,
532    },
533    DeterministicStateInitValueLengthExceeded {
534        length: u64,
535        limit: u64,
536    },
537    GasKeyInvalidNumNonces {
538        limit: u16,
539        requested_nonces: u16,
540    },
541    AddGasKeyWithNonZeroBalance {
542        balance: NearToken,
543    },
544    GasKeyFunctionCallAllowanceNotAllowed,
545}
546
547// ============================================================================
548// Receipt validation errors
549// ============================================================================
550
551/// Error validating a receipt.
552#[derive(Debug, Clone, Deserialize)]
553pub enum ReceiptValidationError {
554    InvalidPredecessorId {
555        account_id: String,
556    },
557    InvalidReceiverId {
558        account_id: String,
559    },
560    InvalidSignerId {
561        account_id: String,
562    },
563    InvalidDataReceiverId {
564        account_id: String,
565    },
566    ReturnedValueLengthExceeded {
567        length: u64,
568        limit: u64,
569    },
570    NumberInputDataDependenciesExceeded {
571        limit: u64,
572        number_of_input_data_dependencies: u64,
573    },
574    ActionsValidation(ActionsValidationError),
575    ReceiptSizeExceeded {
576        limit: u64,
577        size: u64,
578    },
579    InvalidRefundTo {
580        account_id: String,
581    },
582}
583
584// ============================================================================
585// Storage errors
586// ============================================================================
587
588/// Internal storage error.
589#[derive(Debug, Clone, Deserialize)]
590pub enum StorageError {
591    StorageInternalError,
592    MissingTrieValue(MissingTrieValue),
593    UnexpectedTrieValue,
594    StorageInconsistentState(String),
595    FlatStorageBlockNotSupported(String),
596    MemTrieLoadingError(String),
597}
598
599/// Details about a missing trie value.
600#[derive(Debug, Clone, Deserialize)]
601pub struct MissingTrieValue {
602    pub context: MissingTrieValueContext,
603    pub hash: CryptoHash,
604}
605
606/// Context in which a trie value was missing.
607#[derive(Debug, Clone, Deserialize)]
608#[allow(clippy::enum_variant_names)] // Matches nearcore naming
609pub enum MissingTrieValueContext {
610    TrieIterator,
611    TriePrefetchingStorage,
612    TrieMemoryPartialStorage,
613    TrieStorage,
614}
615
616// ============================================================================
617// Misc
618// ============================================================================
619
620/// Reason a deposit cost check failed on a gas key transaction.
621#[derive(Debug, Clone, Deserialize)]
622pub enum DepositCostFailureReason {
623    NotEnoughBalance,
624    LackBalanceForState,
625}