diff --git a/Cargo.lock b/Cargo.lock index 079f12c3ba9..a6a3567b079 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2110,8 +2110,10 @@ dependencies = [ "near-network 0.1.0", "near-pool 0.1.0", "near-primitives 0.1.0", + "near-rpc-error-macro 0.1.0", "near-store 0.1.0", "prometheus 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2200,6 +2202,8 @@ dependencies = [ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "near-crypto 0.1.0", + "near-rpc-error-macro 0.1.0", + "near-vm-errors 0.4.3", "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "reed-solomon-erasure 4.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2209,6 +2213,29 @@ dependencies = [ "sha2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "near-rpc-error-core" +version = "0.1.0" +dependencies = [ + "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "near-rpc-error-macro" +version = "0.1.0" +dependencies = [ + "near-rpc-error-core 0.1.0", + "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "near-runtime-fees" version = "0.4.3" @@ -2252,6 +2279,11 @@ dependencies = [ [[package]] name = "near-vm-errors" version = "0.4.3" +dependencies = [ + "borsh 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", + "near-rpc-error-macro 0.1.0", + "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", +] [[package]] name = "near-vm-logic" @@ -3468,6 +3500,7 @@ dependencies = [ "near-primitives 0.1.0", "near-runtime-fees 0.4.3", "near-store 0.1.0", + "near-vm-errors 0.4.3", "node-runtime 0.0.1", "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index 5356e1c0bb5..62f6b37e096 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,8 @@ members = [ "test-utils/loadtester", "test-utils/state-viewer", "near/", + "tools/rpctypegen/core", + "tools/rpctypegen/macro", "genesis-tools/genesis-csv-to-json", "genesis-tools/genesis-populate", "genesis-tools/keypair-generator", diff --git a/chain/jsonrpc/Cargo.toml b/chain/jsonrpc/Cargo.toml index 0f050ded1e4..7c46872ed2c 100644 --- a/chain/jsonrpc/Cargo.toml +++ b/chain/jsonrpc/Cargo.toml @@ -21,7 +21,6 @@ serde_derive = "1.0" serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } uuid = { version = "~0.8", features = ["v4"] } - borsh = "0.2.10" near-crypto = { path = "../../core/crypto" } @@ -33,3 +32,10 @@ near-client = { path = "../client" } near-network = { path = "../network" } near-pool = { path = "../pool" } near-jsonrpc-client = { path = "client" } +near-rpc-error-macro = { path = "../../tools/rpctypegen/macro" } + +[build-dependencies] +_rand = { package = "rand", version = "0.6.5" } + +[features] +dump_errors_schema = ["near-rpc-error-macro/dump_errors_schema"] diff --git a/chain/jsonrpc/build_errors_schema.sh b/chain/jsonrpc/build_errors_schema.sh new file mode 100755 index 00000000000..59aa28ddf35 --- /dev/null +++ b/chain/jsonrpc/build_errors_schema.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cargo build --features dump_errors_schema +cp ../../target/rpc_errors_schema.json ./res/rpc_errors_schema.json diff --git a/chain/jsonrpc/res/rpc_errors_schema.json b/chain/jsonrpc/res/rpc_errors_schema.json new file mode 100644 index 00000000000..1b97a6b787c --- /dev/null +++ b/chain/jsonrpc/res/rpc_errors_schema.json @@ -0,0 +1,562 @@ +{ + "schema": { + "BadUTF16": { + "name": "BadUTF16", + "subtypes": [], + "props": {} + }, + "BadUTF8": { + "name": "BadUTF8", + "subtypes": [], + "props": {} + }, + "BalanceExceeded": { + "name": "BalanceExceeded", + "subtypes": [], + "props": {} + }, + "CannotAppendActionToJointPromise": { + "name": "CannotAppendActionToJointPromise", + "subtypes": [], + "props": {} + }, + "CannotReturnJointPromise": { + "name": "CannotReturnJointPromise", + "subtypes": [], + "props": {} + }, + "CodeDoesNotExist": { + "name": "CodeDoesNotExist", + "subtypes": [], + "props": { + "account_id": "" + } + }, + "CompilationError": { + "name": "CompilationError", + "subtypes": [ + "CodeDoesNotExist", + "PrepareError", + "WasmerCompileError" + ], + "props": {} + }, + "Deserialization": { + "name": "Deserialization", + "subtypes": [], + "props": {} + }, + "EmptyMethodName": { + "name": "EmptyMethodName", + "subtypes": [], + "props": {} + }, + "FunctionExecError": { + "name": "FunctionExecError", + "subtypes": [ + "CompilationError", + "LinkError", + "MethodResolveError", + "WasmTrap", + "HostError" + ], + "props": {} + }, + "GasExceeded": { + "name": "GasExceeded", + "subtypes": [], + "props": {} + }, + "GasInstrumentation": { + "name": "GasInstrumentation", + "subtypes": [], + "props": {} + }, + "GasLimitExceeded": { + "name": "GasLimitExceeded", + "subtypes": [], + "props": {} + }, + "GuestPanic": { + "name": "GuestPanic", + "subtypes": [], + "props": { + "panic_msg": "" + } + }, + "HostError": { + "name": "HostError", + "subtypes": [ + "BadUTF16", + "BadUTF8", + "GasExceeded", + "GasLimitExceeded", + "BalanceExceeded", + "EmptyMethodName", + "GuestPanic", + "IntegerOverflow", + "InvalidPromiseIndex", + "CannotAppendActionToJointPromise", + "CannotReturnJointPromise", + "InvalidPromiseResultIndex", + "InvalidRegisterId", + "IteratorWasInvalidated", + "MemoryAccessViolation", + "InvalidReceiptIndex", + "InvalidIteratorIndex", + "InvalidAccountId", + "InvalidMethodName", + "InvalidPublicKey", + "ProhibitedInView" + ], + "props": {} + }, + "Instantiate": { + "name": "Instantiate", + "subtypes": [], + "props": {} + }, + "IntegerOverflow": { + "name": "IntegerOverflow", + "subtypes": [], + "props": {} + }, + "InternalMemoryDeclared": { + "name": "InternalMemoryDeclared", + "subtypes": [], + "props": {} + }, + "InvalidAccountId": { + "name": "InvalidAccountId", + "subtypes": [], + "props": {} + }, + "InvalidIteratorIndex": { + "name": "InvalidIteratorIndex", + "subtypes": [], + "props": { + "iterator_index": "" + } + }, + "InvalidMethodName": { + "name": "InvalidMethodName", + "subtypes": [], + "props": {} + }, + "InvalidPromiseIndex": { + "name": "InvalidPromiseIndex", + "subtypes": [], + "props": { + "promise_idx": "" + } + }, + "InvalidPromiseResultIndex": { + "name": "InvalidPromiseResultIndex", + "subtypes": [], + "props": { + "result_idx": "" + } + }, + "InvalidPublicKey": { + "name": "InvalidPublicKey", + "subtypes": [], + "props": {} + }, + "InvalidReceiptIndex": { + "name": "InvalidReceiptIndex", + "subtypes": [], + "props": { + "receipt_index": "" + } + }, + "InvalidRegisterId": { + "name": "InvalidRegisterId", + "subtypes": [], + "props": { + "register_id": "" + } + }, + "IteratorWasInvalidated": { + "name": "IteratorWasInvalidated", + "subtypes": [], + "props": { + "iterator_index": "" + } + }, + "LinkError": { + "name": "LinkError", + "subtypes": [], + "props": { + "msg": "" + } + }, + "Memory": { + "name": "Memory", + "subtypes": [], + "props": {} + }, + "MemoryAccessViolation": { + "name": "MemoryAccessViolation", + "subtypes": [], + "props": {} + }, + "MethodEmptyName": { + "name": "MethodEmptyName", + "subtypes": [], + "props": {} + }, + "MethodInvalidSignature": { + "name": "MethodInvalidSignature", + "subtypes": [], + "props": {} + }, + "MethodNotFound": { + "name": "MethodNotFound", + "subtypes": [], + "props": {} + }, + "MethodResolveError": { + "name": "MethodResolveError", + "subtypes": [ + "MethodEmptyName", + "MethodUTF8Error", + "MethodNotFound", + "MethodInvalidSignature" + ], + "props": {} + }, + "MethodUTF8Error": { + "name": "MethodUTF8Error", + "subtypes": [], + "props": {} + }, + "PrepareError": { + "name": "PrepareError", + "subtypes": [ + "Serialization", + "Deserialization", + "InternalMemoryDeclared", + "GasInstrumentation", + "StackHeightInstrumentation", + "Instantiate", + "Memory" + ], + "props": {} + }, + "ProhibitedInView": { + "name": "ProhibitedInView", + "subtypes": [], + "props": { + "method_name": "" + } + }, + "Serialization": { + "name": "Serialization", + "subtypes": [], + "props": {} + }, + "StackHeightInstrumentation": { + "name": "StackHeightInstrumentation", + "subtypes": [], + "props": {} + }, + "VMError": { + "name": "VMError", + "subtypes": [ + "FunctionExecError", + "StorageError" + ], + "props": {} + }, + "WasmTrap": { + "name": "WasmTrap", + "subtypes": [], + "props": { + "msg": "" + } + }, + "WasmerCompileError": { + "name": "WasmerCompileError", + "subtypes": [], + "props": { + "msg": "" + } + }, + "AccessKeyNotFound": { + "name": "AccessKeyNotFound", + "subtypes": [], + "props": { + "account_id": "", + "public_key": "" + } + }, + "AccountAlreadyExists": { + "name": "AccountAlreadyExists", + "subtypes": [], + "props": { + "account_id": "" + } + }, + "AccountDoesNotExist": { + "name": "AccountDoesNotExist", + "subtypes": [], + "props": { + "account_id": "" + } + }, + "ActionError": { + "name": "ActionError", + "subtypes": [ + "AccountAlreadyExists", + "AccountDoesNotExist", + "CreateAccountNotAllowed", + "ActorNoPermission", + "DeleteKeyDoesNotExist", + "AddKeyAlreadyExists", + "DeleteAccountStaking", + "DeleteAccountHasRent", + "RentUnpaid", + "TriesToUnstake", + "TriesToStake", + "FunctionCall" + ], + "props": { + "index": "" + } + }, + "ActorNoPermission": { + "name": "ActorNoPermission", + "subtypes": [], + "props": { + "account_id": "", + "actor_id": "" + } + }, + "AddKeyAlreadyExists": { + "name": "AddKeyAlreadyExists", + "subtypes": [], + "props": { + "account_id": "", + "public_key": "" + } + }, + "BalanceMismatchError": { + "name": "BalanceMismatchError", + "subtypes": [], + "props": { + "final_accounts_balance": "", + "final_postponed_receipts_balance": "", + "incoming_receipts_balance": "", + "incoming_validator_rewards": "", + "initial_accounts_balance": "", + "initial_postponed_receipts_balance": "", + "new_delayed_receipts_balance": "", + "outgoing_receipts_balance": "", + "processed_delayed_receipts_balance": "", + "total_balance_burnt": "", + "total_balance_slashed": "", + "total_rent_paid": "", + "total_validator_reward": "" + } + }, + "CostOverflow": { + "name": "CostOverflow", + "subtypes": [], + "props": {} + }, + "CreateAccountNotAllowed": { + "name": "CreateAccountNotAllowed", + "subtypes": [], + "props": { + "account_id": "", + "predecessor_id": "" + } + }, + "DeleteAccountHasRent": { + "name": "DeleteAccountHasRent", + "subtypes": [], + "props": { + "account_id": "", + "balance": "" + } + }, + "DeleteAccountStaking": { + "name": "DeleteAccountStaking", + "subtypes": [], + "props": { + "account_id": "" + } + }, + "DeleteKeyDoesNotExist": { + "name": "DeleteKeyDoesNotExist", + "subtypes": [], + "props": { + "account_id": "", + "public_key": "" + } + }, + "Expired": { + "name": "Expired", + "subtypes": [], + "props": {} + }, + "InvalidAccessKeyError": { + "name": "InvalidAccessKeyError", + "subtypes": [ + "AccessKeyNotFound", + "ReceiverMismatch", + "MethodNameMismatch", + "RequiresFullAccess", + "NotEnoughAllowance" + ], + "props": {} + }, + "InvalidChain": { + "name": "InvalidChain", + "subtypes": [], + "props": {} + }, + "InvalidNonce": { + "name": "InvalidNonce", + "subtypes": [], + "props": { + "ak_nonce": "", + "tx_nonce": "" + } + }, + "InvalidReceiverId": { + "name": "InvalidReceiverId", + "subtypes": [], + "props": { + "receiver_id": "" + } + }, + "InvalidSignature": { + "name": "InvalidSignature", + "subtypes": [], + "props": {} + }, + "InvalidSignerId": { + "name": "InvalidSignerId", + "subtypes": [], + "props": { + "signer_id": "" + } + }, + "InvalidTxError": { + "name": "InvalidTxError", + "subtypes": [ + "InvalidAccessKeyError", + "InvalidSignerId", + "SignerDoesNotExist", + "InvalidNonce", + "InvalidReceiverId", + "InvalidSignature", + "NotEnoughBalance", + "RentUnpaid", + "CostOverflow", + "InvalidChain", + "Expired" + ], + "props": {} + }, + "MethodNameMismatch": { + "name": "MethodNameMismatch", + "subtypes": [], + "props": { + "method_name": "" + } + }, + "NotEnoughAllowance": { + "name": "NotEnoughAllowance", + "subtypes": [], + "props": { + "account_id": "", + "allowance": "", + "cost": "", + "public_key": "" + } + }, + "NotEnoughBalance": { + "name": "NotEnoughBalance", + "subtypes": [], + "props": { + "balance": "", + "cost": "", + "signer_id": "" + } + }, + "ReceiverMismatch": { + "name": "ReceiverMismatch", + "subtypes": [], + "props": { + "ak_receiver": "", + "tx_receiver": "" + } + }, + "RentUnpaid": { + "name": "RentUnpaid", + "subtypes": [], + "props": { + "account_id": "", + "amount": "" + } + }, + "SignerDoesNotExist": { + "name": "SignerDoesNotExist", + "subtypes": [], + "props": { + "signer_id": "" + } + }, + "TriesToStake": { + "name": "TriesToStake", + "subtypes": [], + "props": { + "account_id": "", + "balance": "", + "locked": "", + "stake": "" + } + }, + "TriesToUnstake": { + "name": "TriesToUnstake", + "subtypes": [], + "props": { + "account_id": "" + } + }, + "TxExecutionError": { + "name": "TxExecutionError", + "subtypes": [ + "ActionError", + "InvalidTxError" + ], + "props": {} + }, + "Closed": { + "name": "Closed", + "subtypes": [], + "props": {} + }, + "ServerError": { + "name": "ServerError", + "subtypes": [ + "TxExecutionError", + "Timeout", + "Closed" + ], + "props": {} + }, + "Timeout": { + "name": "Timeout", + "subtypes": [], + "props": {} + }, + "RequiresFullAccess": { + "name": "RequiresFullAccess", + "subtypes": [], + "props": {} + } + } +} \ No newline at end of file diff --git a/chain/jsonrpc/src/lib.rs b/chain/jsonrpc/src/lib.rs index 376bb3af24d..6ea0202aa81 100644 --- a/chain/jsonrpc/src/lib.rs +++ b/chain/jsonrpc/src/lib.rs @@ -1,6 +1,7 @@ extern crate prometheus; use std::convert::TryFrom; +use std::fmt::Display; use std::string::FromUtf8Error; use std::time::Duration; @@ -25,11 +26,12 @@ pub use near_jsonrpc_client as client; use near_jsonrpc_client::{message, ChunkId}; use near_metrics::{Encoder, TextEncoder}; use near_network::{NetworkClientMessages, NetworkClientResponses}; +use near_primitives::errors::{InvalidTxError, TxExecutionError}; use near_primitives::hash::CryptoHash; use near_primitives::serialize::{from_base, from_base64, BaseEncode}; use near_primitives::transaction::SignedTransaction; use near_primitives::types::{AccountId, BlockId, MaybeBlockId, StateChangesRequest}; -use near_primitives::views::{ExecutionErrorView, FinalExecutionStatus}; +use near_primitives::views::FinalExecutionStatus; mod metrics; pub mod test_utils; @@ -111,15 +113,47 @@ fn parse_tx(params: Option) -> Result { .map_err(|e| RpcError::invalid_params(Some(format!("Failed to decode transaction: {}", e)))) } -fn convert_mailbox_error(e: MailboxError) -> ExecutionErrorView { - ExecutionErrorView { error_message: e.to_string(), error_type: "MailBoxError".to_string() } +/// A general Server Error +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, near_rpc_error_macro::RpcError)] +pub enum ServerError { + TxExecutionError(TxExecutionError), + Timeout, + Closed, +} + +impl Display for ServerError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + match self { + ServerError::TxExecutionError(e) => write!(f, "ServerError: {}", e), + ServerError::Timeout => write!(f, "ServerError: Timeout"), + ServerError::Closed => write!(f, "ServerError: Closed"), + } + } +} + +impl From for ServerError { + fn from(e: InvalidTxError) -> ServerError { + ServerError::TxExecutionError(TxExecutionError::InvalidTxError(e)) + } +} + +impl From for ServerError { + fn from(e: MailboxError) -> Self { + match e { + MailboxError::Closed => ServerError::Closed, + MailboxError::Timeout => ServerError::Timeout, + } + } +} + +impl From for RpcError { + fn from(e: ServerError) -> RpcError { + RpcError::server_error(Some(e)) + } } fn timeout_err() -> RpcError { - RpcError::server_error(Some(ExecutionErrorView { - error_message: "send_tx_commit has timed out".to_string(), - error_type: "TimeoutError".to_string(), - })) + RpcError::server_error(Some(ServerError::Timeout)) } struct JsonRpcHandler { @@ -199,20 +233,17 @@ impl JsonRpcHandler { let result = self .client_addr .send(NetworkClientMessages::Transaction(tx)) - .map_err(|err| RpcError::server_error(Some(convert_mailbox_error(err)))) + .map_err(|err| RpcError::server_error(Some(ServerError::from(err)))) .await?; match result { NetworkClientResponses::ValidTx | NetworkClientResponses::RequestRouted => { self.tx_polling(tx_hash, signer_account_id).await } NetworkClientResponses::InvalidTx(err) => { - Err(RpcError::server_error(Some(ExecutionErrorView::from(err)))) + Err(RpcError::server_error(Some(ServerError::TxExecutionError(err.into())))) } NetworkClientResponses::NoResponse => { - Err(RpcError::server_error(Some(ExecutionErrorView { - error_message: "send_tx_commit has timed out".to_string(), - error_type: "TimeoutError".to_string(), - }))) + Err(RpcError::server_error(Some(ServerError::Timeout))) } _ => unreachable!(), } diff --git a/core/primitives/Cargo.toml b/core/primitives/Cargo.toml index aaa66b8f0a9..afe5e1077fd 100644 --- a/core/primitives/Cargo.toml +++ b/core/primitives/Cargo.toml @@ -26,9 +26,12 @@ actix = "0.9.0" borsh = "0.2.10" near-crypto = { path = "../crypto" } +near-vm-errors = { path = "../../runtime/near-vm-errors" } +near-rpc-error-macro = { path = "../../tools/rpctypegen/macro" } [features] default = ["jemallocator"] +dump_errors_schema = ["near-rpc-error-macro/dump_errors_schema"] [dev-dependencies] bencher = "0.1.5" diff --git a/core/primitives/src/errors.rs b/core/primitives/src/errors.rs index 2ad2b309b7a..679da83c88b 100644 --- a/core/primitives/src/errors.rs +++ b/core/primitives/src/errors.rs @@ -1,10 +1,62 @@ +use crate::serialize::u128_dec_format; use crate::types::{AccountId, Balance, Nonce}; use borsh::{BorshDeserialize, BorshSerialize}; use near_crypto::PublicKey; +use serde::{Deserialize, Serialize}; use std::fmt::Display; +use near_rpc_error_macro::RpcError; +use near_vm_errors::VMError; + +/// Error returned in the ExecutionOutcome in case of failure +#[derive( + BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq, Eq, Deserialize, Serialize, RpcError, +)] +pub enum TxExecutionError { + /// An error happened during Acton execution + ActionError(ActionError), + /// An error happened during Transaction execution + InvalidTxError(InvalidTxError), +} + +impl Display for TxExecutionError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + match self { + TxExecutionError::ActionError(e) => write!(f, "{}", e), + TxExecutionError::InvalidTxError(e) => write!(f, "{}", e), + } + } +} + +impl From for TxExecutionError { + fn from(error: ActionError) -> Self { + TxExecutionError::ActionError(error) + } +} + +impl From for TxExecutionError { + fn from(error: InvalidTxError) -> Self { + TxExecutionError::InvalidTxError(error) + } +} + +/// Error returned from `Runtime::apply` +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RuntimeError { + /// An unexpected integer overflow occurred. The likely issue is an invalid state or the transition. + UnexpectedIntegerOverflow, + /// An error happened during TX verification and account charging. It's likely the chunk is invalid. + /// and should be challenged. + InvalidTxError(InvalidTxError), + /// Unexpected error which is typically related to the node storage corruption.account + /// That it's possible the input state is invalid or malicious. + StorageError(StorageError), + /// An error happens if `check_balance` fails, which is likely an indication of an invalid state. + BalanceMismatchError(BalanceMismatchError), +} + /// Internal -#[derive(BorshSerialize, BorshDeserialize, Serialize, Debug, Clone, PartialEq, Eq)] +#[derive(BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq, Eq)] pub enum StorageError { /// Key-value db internal failure StorageInternalError, @@ -26,74 +78,164 @@ impl std::fmt::Display for StorageError { impl std::error::Error for StorageError {} -/// External -#[derive(BorshSerialize, BorshDeserialize, Serialize, Debug, Clone, PartialEq, Eq)] +/// An error happened during TX execution +#[derive( + BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq, Eq, Deserialize, Serialize, RpcError, +)] pub enum InvalidTxError { - InvalidSigner(AccountId), - SignerDoesNotExist(AccountId), - InvalidAccessKey(InvalidAccessKeyError), - InvalidNonce(Nonce, Nonce), - InvalidReceiver(AccountId), + /// Happens if a wrong AccessKey used or AccessKey has not enough permissions + InvalidAccessKeyError(InvalidAccessKeyError), + /// TX signer_id is not in a valid format or not satisfy requirements see `near_core::primitives::utils::is_valid_account_id` + InvalidSignerId { signer_id: AccountId }, + /// TX signer_id is not found in a storage + SignerDoesNotExist { signer_id: AccountId }, + /// Transaction nonce must be account[access_key].nonce + 1 + InvalidNonce { tx_nonce: Nonce, ak_nonce: Nonce }, + /// TX receiver_id is not in a valid format or not satisfy requirements see `near_core::primitives::utils::is_valid_account_id` + InvalidReceiverId { receiver_id: AccountId }, + /// TX signature is not valid InvalidSignature, - NotEnoughBalance(AccountId, Balance, Balance), - RentUnpaid(AccountId, Balance), + /// Account does not have enough balance to cover TX cost + NotEnoughBalance { + signer_id: AccountId, + #[serde(with = "u128_dec_format")] + balance: Balance, + #[serde(with = "u128_dec_format")] + cost: Balance, + }, + /// Signer account rent is unpaid + RentUnpaid { + /// An account which is required to pay the rent + signer_id: AccountId, + /// Required balance to cover the state rent + #[serde(with = "u128_dec_format")] + amount: Balance, + }, + /// An integer overflow occurred during transaction cost estimation. CostOverflow, + /// Transaction parent block hash doesn't belong to the current chain InvalidChain, + /// Transaction has expired Expired, } -#[derive(BorshSerialize, BorshDeserialize, Serialize, Debug, Clone, PartialEq, Eq)] +#[derive( + BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq, Eq, Deserialize, Serialize, RpcError, +)] pub enum InvalidAccessKeyError { - AccessKeyNotFound(AccountId, PublicKey), - ReceiverMismatch(AccountId, AccountId), - MethodNameMismatch(String), - ActionError, - NotEnoughAllowance(AccountId, PublicKey, Balance, Balance), + /// The access key identified by the `public_key` doesn't exist for the account + AccessKeyNotFound { account_id: AccountId, public_key: PublicKey }, + /// Transaction `receiver_id` doesn't match the access key receiver_id + ReceiverMismatch { tx_receiver: AccountId, ak_receiver: AccountId }, + /// Transaction method name isn't allowed by the access key + MethodNameMismatch { method_name: String }, + /// Transaction requires a full permission access key. + RequiresFullAccess, + /// Access Key does not have enough allowance to cover transaction cost + NotEnoughAllowance { + account_id: AccountId, + public_key: PublicKey, + #[serde(with = "u128_dec_format")] + allowance: Balance, + #[serde(with = "u128_dec_format")] + cost: Balance, + }, } -#[derive(BorshSerialize, BorshDeserialize, Serialize, Debug, Clone, PartialEq, Eq)] -pub enum ActionError { - AccountAlreadyExists(AccountId), - AccountDoesNotExist(String, AccountId), - CreateAccountNotAllowed(AccountId, AccountId), - ActorNoPermission(AccountId, AccountId, String), - DeleteKeyDoesNotExist(AccountId), - AddKeyAlreadyExists(PublicKey), - DeleteAccountStaking(AccountId), - DeleteAccountHasRent(AccountId, Balance), - RentUnpaid(AccountId, Balance), - TriesToUnstake(AccountId), - TriesToStake(AccountId, Balance, Balance, Balance), - FunctionCallError(String), // TODO type +/// An error happened during Acton execution +#[derive( + BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq, Eq, Deserialize, Serialize, RpcError, +)] +pub struct ActionError { + /// Index of the failed action in the transaction. + /// Action index is not defined if ActionError.kind is `ActionErrorKind::RentUnpaid` + pub index: Option, + /// The kind of ActionError happened + pub kind: ActionErrorKind, +} + +#[derive( + BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq, Eq, Deserialize, Serialize, RpcError, +)] +pub enum ActionErrorKind { + /// Happens when CreateAccount action tries to create an account with account_id which is already exists in the storage + AccountAlreadyExists { account_id: AccountId }, + /// Happens when TX receiver_id doesn't exist (but action is not Action::CreateAccount) + AccountDoesNotExist { account_id: AccountId }, + /// A newly created account must be under a namespace of the creator account + CreateAccountNotAllowed { account_id: AccountId, predecessor_id: AccountId }, + /// Administrative actions like `DeployContract`, `Stake`, `AddKey`, `DeleteKey`. can be proceed only if sender=receiver + /// or the first TX action is a `CreateAccount` action + ActorNoPermission { account_id: AccountId, actor_id: AccountId }, + /// Account tries to remove an access key that doesn't exist + DeleteKeyDoesNotExist { account_id: AccountId, public_key: PublicKey }, + /// The public key is already used for an existing access key + AddKeyAlreadyExists { account_id: AccountId, public_key: PublicKey }, + /// Account is staking and can not be deleted + DeleteAccountStaking { account_id: AccountId }, + /// Foreign sender (sender=!receiver) can delete an account only if a target account hasn't enough tokens to pay rent + DeleteAccountHasRent { + account_id: AccountId, + #[serde(with = "u128_dec_format")] + balance: Balance, + }, + /// ActionReceipt can't be completed, because the remaining balance will not be enough to pay rent. + RentUnpaid { + /// An account which is required to pay the rent + account_id: AccountId, + /// Rent due to pay. + #[serde(with = "u128_dec_format")] + amount: Balance, + }, + /// Account is not yet staked, but tries to unstake + TriesToUnstake { account_id: AccountId }, + /// The account doesn't have enough balance to increase the stake. + TriesToStake { + account_id: AccountId, + #[serde(with = "u128_dec_format")] + stake: Balance, + #[serde(with = "u128_dec_format")] + locked: Balance, + #[serde(with = "u128_dec_format")] + balance: Balance, + }, + /// An error occurred during a `FunctionCall` Action. + FunctionCall(VMError), +} + +impl From for ActionError { + fn from(e: ActionErrorKind) -> ActionError { + ActionError { index: None, kind: e } + } } impl Display for InvalidTxError { fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { match self { - InvalidTxError::InvalidSigner(signer_id) => { + InvalidTxError::InvalidSignerId{signer_id} => { write!(f, "Invalid signer account ID {:?} according to requirements", signer_id) } - InvalidTxError::SignerDoesNotExist(signer_id) => { + InvalidTxError::SignerDoesNotExist{signer_id} => { write!(f, "Signer {:?} does not exist", signer_id) } - InvalidTxError::InvalidAccessKey(access_key_error) => access_key_error.fmt(f), - InvalidTxError::InvalidNonce(tx_nonce, ak_nonce) => write!( + InvalidTxError::InvalidAccessKeyError(access_key_error) => access_key_error.fmt(f), + InvalidTxError::InvalidNonce{tx_nonce, ak_nonce} => write!( f, "Transaction nonce {} must be larger than nonce of the used access key {}", tx_nonce, ak_nonce ), - InvalidTxError::InvalidReceiver(receiver_id) => { + InvalidTxError::InvalidReceiverId{receiver_id} => { write!(f, "Invalid receiver account ID {:?} according to requirements", receiver_id) } InvalidTxError::InvalidSignature => { write!(f, "Transaction is not signed with the given public key") } - InvalidTxError::NotEnoughBalance(signer_id, balance, cost) => write!( + InvalidTxError::NotEnoughBalance{signer_id, balance, cost} => write!( f, "Sender {:?} does not have enough balance {} for operation costing {}", signer_id, balance, cost ), - InvalidTxError::RentUnpaid(signer_id, amount) => { + InvalidTxError::RentUnpaid{ signer_id, amount} => { write!(f, "Failed to execute, because the account {:?} wouldn't have enough to pay required rent {}", signer_id, amount) } InvalidTxError::CostOverflow => { @@ -111,59 +253,80 @@ impl Display for InvalidTxError { impl From for InvalidTxError { fn from(error: InvalidAccessKeyError) -> Self { - InvalidTxError::InvalidAccessKey(error) + InvalidTxError::InvalidAccessKeyError(error) } } impl Display for InvalidAccessKeyError { fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { match self { - InvalidAccessKeyError::AccessKeyNotFound(account_id, public_key) => write!( + InvalidAccessKeyError::AccessKeyNotFound { account_id, public_key } => write!( f, "Signer {:?} doesn't have access key with the given public_key {}", account_id, public_key ), - InvalidAccessKeyError::ReceiverMismatch(tx_receiver, ak_receiver) => write!( + InvalidAccessKeyError::ReceiverMismatch { tx_receiver, ak_receiver } => write!( f, "Transaction receiver_id {:?} doesn't match the access key receiver_id {:?}", tx_receiver, ak_receiver ), - InvalidAccessKeyError::MethodNameMismatch(method_name) => write!( + InvalidAccessKeyError::MethodNameMismatch { method_name } => write!( f, "Transaction method name {:?} isn't allowed by the access key", method_name ), - InvalidAccessKeyError::ActionError => { - write!(f, "The used access key requires exactly one FunctionCall action") - } - InvalidAccessKeyError::NotEnoughAllowance(account_id, public_key, allowance, cost) => { - write!( - f, - "Access Key {:?}:{} does not have enough balance {} for transaction costing {}", - account_id, public_key, allowance, cost - ) - } + InvalidAccessKeyError::RequiresFullAccess => write!( + f, + "The transaction contains more then one action, but it was signed \ + with an access key which allows transaction to apply only one specific action. \ + To apply more then one actions TX must be signed with a full access key" + ), + InvalidAccessKeyError::NotEnoughAllowance { + account_id, + public_key, + allowance, + cost, + } => write!( + f, + "Access Key {:?}:{} does not have enough balance {} for transaction costing {}", + account_id, public_key, allowance, cost + ), } } } /// Happens when the input balance doesn't match the output balance in Runtime apply. -#[derive(BorshSerialize, BorshDeserialize, Serialize, Debug, Clone, PartialEq, Eq)] +#[derive( + BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq, Eq, Deserialize, Serialize, RpcError, +)] pub struct BalanceMismatchError { // Input balances + #[serde(with = "u128_dec_format")] pub incoming_validator_rewards: Balance, + #[serde(with = "u128_dec_format")] pub initial_accounts_balance: Balance, + #[serde(with = "u128_dec_format")] pub incoming_receipts_balance: Balance, + #[serde(with = "u128_dec_format")] pub processed_delayed_receipts_balance: Balance, + #[serde(with = "u128_dec_format")] pub initial_postponed_receipts_balance: Balance, // Output balances + #[serde(with = "u128_dec_format")] pub final_accounts_balance: Balance, + #[serde(with = "u128_dec_format")] pub outgoing_receipts_balance: Balance, + #[serde(with = "u128_dec_format")] pub new_delayed_receipts_balance: Balance, + #[serde(with = "u128_dec_format")] pub final_postponed_receipts_balance: Balance, + #[serde(with = "u128_dec_format")] pub total_rent_paid: Balance, + #[serde(with = "u128_dec_format")] pub total_validator_reward: Balance, + #[serde(with = "u128_dec_format")] pub total_balance_burnt: Balance, + #[serde(with = "u128_dec_format")] pub total_balance_slashed: Balance, } @@ -222,18 +385,9 @@ impl Display for BalanceMismatchError { } } -#[derive(BorshSerialize, BorshDeserialize, Serialize, Debug, Clone, PartialEq, Eq)] +#[derive(BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq, Eq)] pub struct IntegerOverflowError; -/// Error returned from `Runtime::apply` -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum RuntimeError { - UnexpectedIntegerOverflow, - InvalidTxError(InvalidTxError), - StorageError(StorageError), - BalanceMismatch(BalanceMismatchError), -} - impl From for InvalidTxError { fn from(_: IntegerOverflowError) -> Self { InvalidTxError::CostOverflow @@ -254,7 +408,7 @@ impl From for RuntimeError { impl From for RuntimeError { fn from(e: BalanceMismatchError) -> Self { - RuntimeError::BalanceMismatch(e) + RuntimeError::BalanceMismatchError(e) } } @@ -265,86 +419,65 @@ impl From for RuntimeError { } impl Display for ActionError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + write!(f, "Action #{}: {}", self.index.unwrap_or_default(), self.kind) + } +} + +impl Display for ActionErrorKind { fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { match self { - ActionError::AccountAlreadyExists(account_id) => { + ActionErrorKind::AccountAlreadyExists { account_id } => { write!(f, "Can't create a new account {:?}, because it already exists", account_id) } - ActionError::AccountDoesNotExist(action, account_id) => write!( + + ActionErrorKind::AccountDoesNotExist { account_id } => write!( f, - "Can't complete the action {:?}, because account {:?} doesn't exist", - action, account_id + "Can't complete the action because account {:?} doesn't exist", + account_id ), - ActionError::ActorNoPermission(actor_id, account_id, action) => write!( + ActionErrorKind::ActorNoPermission { actor_id, account_id } => write!( f, - "Actor {:?} doesn't have permission to account {:?} to complete the action {:?}", - actor_id, account_id, action + "Actor {:?} doesn't have permission to account {:?} to complete the action", + actor_id, account_id ), - ActionError::RentUnpaid(account_id, amount) => write!( + ActionErrorKind::RentUnpaid { account_id, amount } => write!( f, "The account {} wouldn't have enough balance to pay required rent {}", account_id, amount ), - ActionError::TriesToUnstake(account_id) => { + ActionErrorKind::TriesToUnstake { account_id } => { write!(f, "Account {:?} is not yet staked, but tries to unstake", account_id) } - ActionError::TriesToStake(account_id, stake, staked, balance) => write!( + ActionErrorKind::TriesToStake { account_id, stake, locked, balance } => write!( f, "Account {:?} tries to stake {}, but has staked {} and only has {}", - account_id, stake, staked, balance + account_id, stake, locked, balance ), - ActionError::CreateAccountNotAllowed(account_id, predecessor_id) => write!( + ActionErrorKind::CreateAccountNotAllowed { account_id, predecessor_id } => write!( f, "The new account_id {:?} can't be created by {:?}", account_id, predecessor_id ), - ActionError::DeleteKeyDoesNotExist(account_id) => write!( + ActionErrorKind::DeleteKeyDoesNotExist { account_id, .. } => write!( f, "Account {:?} tries to remove an access key that doesn't exist", account_id ), - ActionError::AddKeyAlreadyExists(public_key) => write!( + ActionErrorKind::AddKeyAlreadyExists { public_key, .. } => write!( f, "The public key {:?} is already used for an existing access key", public_key ), - ActionError::DeleteAccountStaking(account_id) => { + ActionErrorKind::DeleteAccountStaking { account_id } => { write!(f, "Account {:?} is staking and can not be deleted", account_id) } - ActionError::DeleteAccountHasRent(account_id, balance) => write!( + ActionErrorKind::DeleteAccountHasRent { account_id, balance } => write!( f, "Account {:?} can't be deleted. It has {}, which is enough to cover the rent", account_id, balance ), - ActionError::FunctionCallError(s) => write!(f, "{}", s), + ActionErrorKind::FunctionCall(s) => write!(f, "{}", s), } } } - -/// Error returned in the ExecutionOutcome in case of failure. -#[derive(BorshSerialize, BorshDeserialize, Serialize, Debug, Clone, PartialEq, Eq)] -pub enum ExecutionError { - Action(ActionError), - InvalidTx(InvalidTxError), -} - -impl Display for ExecutionError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { - match self { - ExecutionError::Action(e) => write!(f, "{}", e), - ExecutionError::InvalidTx(e) => write!(f, "{}", e), - } - } -} - -impl From for ExecutionError { - fn from(error: ActionError) -> Self { - ExecutionError::Action(error) - } -} - -impl From for ExecutionError { - fn from(error: InvalidTxError) -> Self { - ExecutionError::InvalidTx(error) - } -} diff --git a/core/primitives/src/transaction.rs b/core/primitives/src/transaction.rs index f7b674cea51..75861175cc0 100644 --- a/core/primitives/src/transaction.rs +++ b/core/primitives/src/transaction.rs @@ -6,7 +6,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use near_crypto::{PublicKey, Signature, Signer}; use crate::account::AccessKey; -use crate::errors::ExecutionError; +use crate::errors::TxExecutionError; use crate::hash::{hash, CryptoHash}; use crate::logging; use crate::merkle::MerklePath; @@ -211,7 +211,7 @@ pub enum ExecutionStatus { /// The execution is pending or unknown. Unknown, /// The execution has failed with the given execution error. - Failure(ExecutionError), + Failure(TxExecutionError), /// The final action succeeded and returned some value or an empty vec. SuccessValue(Vec), /// The final action of the receipt returned a promise or the signed transaction was converted diff --git a/core/primitives/src/views.rs b/core/primitives/src/views.rs index 35c05ca4e0e..02d97c68088 100644 --- a/core/primitives/src/views.rs +++ b/core/primitives/src/views.rs @@ -10,7 +10,7 @@ use near_crypto::{PublicKey, Signature}; use crate::account::{AccessKey, AccessKeyPermission, Account, FunctionCallPermission}; use crate::block::{Approval, Block, BlockHeader, BlockHeaderInnerLite, BlockHeaderInnerRest}; use crate::challenge::{Challenge, ChallengesResult}; -use crate::errors::{ActionError, ExecutionError, InvalidAccessKeyError, InvalidTxError}; +use crate::errors::TxExecutionError; use crate::hash::{hash, CryptoHash}; use crate::logging; use crate::merkle::MerklePath; @@ -650,7 +650,7 @@ pub enum FinalExecutionStatus { /// The execution has started and still going. Started, /// The execution has failed with the given error. - Failure(ExecutionErrorView), + Failure(TxExecutionError), /// The execution has succeeded and returned some value or an empty vec encoded in base64. SuccessValue(String), } @@ -676,104 +676,10 @@ impl Default for FinalExecutionStatus { } #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] -pub struct ExecutionErrorView { - pub error_message: String, - pub error_type: String, -} - -impl From for ExecutionErrorView { - fn from(error: ExecutionError) -> Self { - ExecutionErrorView { - error_message: format!("{}", error), - error_type: match &error { - ExecutionError::Action(e) => match e { - ActionError::AccountAlreadyExists(_) => { - "ActionError::AccountAlreadyExists".to_string() - } - ActionError::AccountDoesNotExist(_, _) => { - "ActionError::AccountDoesNotExist".to_string() - } - ActionError::CreateAccountNotAllowed(_, _) => { - "ActionError::CreateAccountNotAllowed".to_string() - } - ActionError::ActorNoPermission(_, _, _) => { - "ActionError::ActorNoPermission".to_string() - } - ActionError::DeleteKeyDoesNotExist(_) => { - "ActionError::DeleteKeyDoesNotExist".to_string() - } - ActionError::AddKeyAlreadyExists(_) => { - "ActionError::AddKeyAlreadyExists".to_string() - } - ActionError::DeleteAccountStaking(_) => { - "ActionError::DeleteAccountStaking".to_string() - } - ActionError::DeleteAccountHasRent(_, _) => { - "ActionError::DeleteAccountHasRent".to_string() - } - ActionError::RentUnpaid(_, _) => "ActionError::RentUnpaid".to_string(), - ActionError::TriesToUnstake(_) => "ActionError::TriesToUnstake".to_string(), - ActionError::TriesToStake(_, _, _, _) => { - "ActionError::TriesToStake".to_string() - } - ActionError::FunctionCallError(_) => { - "ActionError::FunctionCallError".to_string() - } - }, - ExecutionError::InvalidTx(e) => match e { - InvalidTxError::InvalidSigner(_) => "InvalidTxError::InvalidSigner".to_string(), - InvalidTxError::SignerDoesNotExist(_) => { - "InvalidTxError::SignerDoesNotExist".to_string() - } - InvalidTxError::InvalidAccessKey(e) => match e { - InvalidAccessKeyError::AccessKeyNotFound(_, _) => { - "InvalidTxError::InvalidAccessKey::AccessKeyNotFound".to_string() - } - InvalidAccessKeyError::ReceiverMismatch(_, _) => { - "InvalidTxError::InvalidAccessKey::ReceiverMismatch".to_string() - } - InvalidAccessKeyError::MethodNameMismatch(_) => { - "InvalidTxError::InvalidAccessKey::MethodNameMismatch".to_string() - } - InvalidAccessKeyError::ActionError => { - "InvalidTxError::InvalidAccessKey::ActionError".to_string() - } - InvalidAccessKeyError::NotEnoughAllowance(_, _, _, _) => { - "InvalidTxError::InvalidAccessKey::NotEnoughAllowance".to_string() - } - }, - InvalidTxError::InvalidNonce(_, _) => { - "InvalidTxError::InvalidNonce".to_string() - } - InvalidTxError::InvalidReceiver(_) => { - "InvalidTxError::InvalidReceiver".to_string() - } - InvalidTxError::InvalidSignature => { - "InvalidTxError::InvalidSignature".to_string() - } - InvalidTxError::NotEnoughBalance(_, _, _) => { - "InvalidTxError::NotEnoughBalance".to_string() - } - InvalidTxError::RentUnpaid(_, _) => "InvalidTxError::RentUnpaid".to_string(), - InvalidTxError::CostOverflow => "InvalidTxError::CostOverflow".to_string(), - InvalidTxError::InvalidChain => "InvalidTxError::InvalidChain".to_string(), - InvalidTxError::Expired => "InvalidTxError::Expired".to_string(), - }, - }, - } - } -} - -impl From for ExecutionErrorView { - fn from(error: ActionError) -> Self { - ExecutionError::Action(error).into() - } -} - -impl From for ExecutionErrorView { - fn from(error: InvalidTxError) -> Self { - ExecutionError::InvalidTx(error).into() - } +pub enum ServerError { + TxExecutionError(TxExecutionError), + Timeout, + Closed, } #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, PartialEq, Eq, Clone)] @@ -781,7 +687,7 @@ pub enum ExecutionStatusView { /// The execution is pending or unknown. Unknown, /// The execution has failed. - Failure(ExecutionErrorView), + Failure(TxExecutionError), /// The final action succeeded and returned some value or an empty vec encoded in base64. SuccessValue(String), /// The final action of the receipt returned a promise or the signed transaction was converted @@ -809,7 +715,7 @@ impl From for ExecutionStatusView { fn from(outcome: ExecutionStatus) -> Self { match outcome { ExecutionStatus::Unknown => ExecutionStatusView::Unknown, - ExecutionStatus::Failure(e) => ExecutionStatusView::Failure(e.into()), + ExecutionStatus::Failure(e) => ExecutionStatusView::Failure(e), ExecutionStatus::SuccessValue(v) => ExecutionStatusView::SuccessValue(to_base64(&v)), ExecutionStatus::SuccessReceiptId(receipt_id) => { ExecutionStatusView::SuccessReceiptId(receipt_id) diff --git a/near/src/runtime.rs b/near/src/runtime.rs index ad52c95e052..b588b05aef2 100644 --- a/near/src/runtime.rs +++ b/near/src/runtime.rs @@ -306,7 +306,7 @@ impl NightshadeRuntime { ) .map_err(|e| match e { RuntimeError::InvalidTxError(_) => ErrorKind::InvalidTransactions, - RuntimeError::BalanceMismatch(e) => panic!("{}", e), + RuntimeError::BalanceMismatchError(e) => panic!("{}", e), // TODO: process gracefully RuntimeError::UnexpectedIntegerOverflow => { panic!("RuntimeError::UnexpectedIntegerOverflow") diff --git a/runtime/near-vm-errors/Cargo.toml b/runtime/near-vm-errors/Cargo.toml index 72f6b34ffca..03d0941fa57 100644 --- a/runtime/near-vm-errors/Cargo.toml +++ b/runtime/near-vm-errors/Cargo.toml @@ -13,3 +13,9 @@ Error that can occur inside Near Runtime encapsulated in a separate crate. Might """ [dependencies] +borsh = "0.2.10" +near-rpc-error-macro = { path = "../../tools/rpctypegen/macro" } +serde = { version = "1.0", features = ["derive"] } + +[features] +dump_errors_schema = ["near-rpc-error-macro/dump_errors_schema"] diff --git a/runtime/near-vm-errors/src/lib.rs b/runtime/near-vm-errors/src/lib.rs index 014ec376f11..ec67da2d937 100644 --- a/runtime/near-vm-errors/src/lib.rs +++ b/runtime/near-vm-errors/src/lib.rs @@ -1,21 +1,31 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use near_rpc_error_macro::RpcError; +use serde::{Deserialize, Serialize}; use std::fmt; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive( + Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize, Deserialize, Serialize, RpcError, +)] pub enum VMError { - FunctionCallError(FunctionCallError), + FunctionExecError(FunctionExecError), + // TODO: serialize/deserialize? StorageError(Vec), } -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum FunctionCallError { +#[derive( + Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize, Deserialize, Serialize, RpcError, +)] +pub enum FunctionExecError { CompilationError(CompilationError), - LinkError(String), - ResolveError(MethodResolveError), - WasmTrap(String), + LinkError { msg: String }, + MethodResolveError(MethodResolveError), + WasmTrap { msg: String }, HostError(HostError), } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive( + Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize, Deserialize, Serialize, RpcError, +)] pub enum MethodResolveError { MethodEmptyName, MethodUTF8Error, @@ -23,71 +33,92 @@ pub enum MethodResolveError { MethodInvalidSignature, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive( + Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize, Deserialize, Serialize, RpcError, +)] pub enum CompilationError { - CodeDoesNotExist(String), + CodeDoesNotExist { account_id: String }, PrepareError(PrepareError), - WasmerCompileError(String), + WasmerCompileError { msg: String }, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive( + Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize, Deserialize, Serialize, RpcError, +)] /// Error that can occur while preparing or executing Wasm smart-contract. pub enum PrepareError { /// Error happened while serializing the module. Serialization, - /// Error happened while deserializing the module. Deserialization, - /// Internal memory declaration has been found in the module. InternalMemoryDeclared, - /// Gas instrumentation failed. /// /// This most likely indicates the module isn't valid. GasInstrumentation, - /// Stack instrumentation failed. /// /// This most likely indicates the module isn't valid. StackHeightInstrumentation, - /// Error happened during instantiation. /// /// This might indicate that `start` function trapped, or module isn't /// instantiable and/or unlinkable. Instantiate, - /// Error creating memory. Memory, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive( + Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize, Deserialize, Serialize, RpcError, +)] pub enum HostError { + /// String encoding is bad UTF-16 sequence BadUTF16, + /// String encoding is bad UTF-8 sequence BadUTF8, + /// Exceeded the prepaid gas GasExceeded, + /// Exceeded the maximum amount of gas allowed to burn per contract GasLimitExceeded, + /// Exceeded the account balance BalanceExceeded, + /// Tried to call an empty method name EmptyMethodName, - GuestPanic(String), + /// Smart contract panicked + GuestPanic { panic_msg: String }, + /// IntegerOverflow happened during a contract execution IntegerOverflow, - InvalidPromiseIndex(u64), + /// `promise_idx` does not correspond to existing promises + InvalidPromiseIndex { promise_idx: u64 }, + /// Actions can only be appended to non-joint promise. CannotAppendActionToJointPromise, + /// Returning joint promise is currently prohibited CannotReturnJointPromise, - InvalidPromiseResultIndex(u64), - InvalidRegisterId(u64), - IteratorWasInvalidated(u64), + /// Accessed invalid promise result index + InvalidPromiseResultIndex { result_idx: u64 }, + /// Accessed invalid register id + InvalidRegisterId { register_id: u64 }, + /// Iterator `iterator_index` was invalidated after its creation by performing a mutable operation on trie + IteratorWasInvalidated { iterator_index: u64 }, + /// Accessed memory outside the bounds MemoryAccessViolation, - InvalidReceiptIndex(u64), - InvalidIteratorIndex(u64), + /// VM Logic returned an invalid receipt index + InvalidReceiptIndex { receipt_index: u64 }, + /// Iterator index `iterator_index` does not exist + InvalidIteratorIndex { iterator_index: u64 }, + /// VM Logic returned an invalid account id InvalidAccountId, + /// VM Logic returned an invalid method name InvalidMethodName, + /// VM Logic provided an invalid public key InvalidPublicKey, - ProhibitedInView(String), + /// `method_name` is not allowed in view calls + ProhibitedInView { method_name: String }, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, BorshDeserialize, BorshSerialize, Deserialize, Serialize)] pub enum HostErrorOrStorageError { HostError(HostError), /// Error from underlying storage, serialized @@ -102,7 +133,7 @@ impl From for HostErrorOrStorageError { impl From for VMError { fn from(err: PrepareError) -> Self { - VMError::FunctionCallError(FunctionCallError::CompilationError( + VMError::FunctionExecError(FunctionExecError::CompilationError( CompilationError::PrepareError(err), )) } @@ -125,14 +156,14 @@ impl fmt::Display for PrepareError { } } -impl fmt::Display for FunctionCallError { +impl fmt::Display for FunctionExecError { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { match self { - FunctionCallError::CompilationError(e) => e.fmt(f), - FunctionCallError::ResolveError(e) => e.fmt(f), - FunctionCallError::HostError(e) => e.fmt(f), - FunctionCallError::LinkError(s) => write!(f, "{}", s), - FunctionCallError::WasmTrap(s) => write!(f, "WebAssembly trap: {}", s), + FunctionExecError::CompilationError(e) => e.fmt(f), + FunctionExecError::MethodResolveError(e) => e.fmt(f), + FunctionExecError::HostError(e) => e.fmt(f), + FunctionExecError::LinkError { msg } => write!(f, "{}", msg), + FunctionExecError::WasmTrap { msg } => write!(f, "WebAssembly trap: {}", msg), } } } @@ -140,11 +171,13 @@ impl fmt::Display for FunctionCallError { impl fmt::Display for CompilationError { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { match self { - CompilationError::CodeDoesNotExist(account_id) => { + CompilationError::CodeDoesNotExist { account_id } => { write!(f, "cannot find contract code for account {}", account_id) } CompilationError::PrepareError(p) => write!(f, "PrepareError: {}", p), - CompilationError::WasmerCompileError(s) => write!(f, "Wasmer compilation error: {}", s), + CompilationError::WasmerCompileError { msg } => { + write!(f, "Wasmer compilation error: {}", msg) + } } } } @@ -158,7 +191,7 @@ impl fmt::Display for MethodResolveError { impl fmt::Display for VMError { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { match self { - VMError::FunctionCallError(err) => fmt::Display::fmt(err, f), + VMError::FunctionExecError(err) => fmt::Display::fmt(err, f), VMError::StorageError(_err) => write!(f, "StorageError"), } } @@ -174,41 +207,41 @@ impl std::fmt::Display for HostError { GasLimitExceeded => write!(f, "Exceeded the maximum amount of gas allowed to burn per contract."), BalanceExceeded => write!(f, "Exceeded the account balance."), EmptyMethodName => write!(f, "Tried to call an empty method name."), - GuestPanic(s) => write!(f, "Smart contract panicked: {}", s), + GuestPanic{ panic_msg } => write!(f, "Smart contract panicked: {}", panic_msg), IntegerOverflow => write!(f, "Integer overflow."), - InvalidIteratorIndex(index) => write!(f, "Iterator index {:?} does not exist", index), - InvalidPromiseIndex(index) => write!(f, "{:?} does not correspond to existing promises", index), + InvalidIteratorIndex{iterator_index} => write!(f, "Iterator index {:?} does not exist", iterator_index), + InvalidPromiseIndex{promise_idx} => write!(f, "{:?} does not correspond to existing promises", promise_idx), CannotAppendActionToJointPromise => write!(f, "Actions can only be appended to non-joint promise."), CannotReturnJointPromise => write!(f, "Returning joint promise is currently prohibited."), - InvalidPromiseResultIndex(index) => write!(f, "Accessed invalid promise result index: {:?}", index), - InvalidRegisterId(id) => write!(f, "Accessed invalid register id: {:?}", id), - IteratorWasInvalidated(index) => write!(f, "Iterator {:?} was invalidated after its creation by performing a mutable operation on trie", index), + InvalidPromiseResultIndex{result_idx} => write!(f, "Accessed invalid promise result index: {:?}", result_idx), + InvalidRegisterId{register_id} => write!(f, "Accessed invalid register id: {:?}", register_id), + IteratorWasInvalidated{iterator_index} => write!(f, "Iterator {:?} was invalidated after its creation by performing a mutable operation on trie", iterator_index), MemoryAccessViolation => write!(f, "Accessed memory outside the bounds."), - InvalidReceiptIndex(index) => write!(f, "VM Logic returned an invalid receipt index: {:?}", index), + InvalidReceiptIndex{receipt_index} => write!(f, "VM Logic returned an invalid receipt index: {:?}", receipt_index), InvalidAccountId => write!(f, "VM Logic returned an invalid account id"), InvalidMethodName => write!(f, "VM Logic returned an invalid method name"), InvalidPublicKey => write!(f, "VM Logic provided an invalid public key"), - ProhibitedInView(method_name) => write!(f, "{} is not allowed in view calls", method_name), + ProhibitedInView{method_name} => write!(f, "{} is not allowed in view calls", method_name), } } } #[cfg(test)] mod tests { - use crate::{CompilationError, FunctionCallError, MethodResolveError, PrepareError, VMError}; + use crate::{CompilationError, FunctionExecError, MethodResolveError, PrepareError, VMError}; #[test] fn test_display() { // TODO: proper printing assert_eq!( - VMError::FunctionCallError(FunctionCallError::ResolveError( + VMError::FunctionExecError(FunctionExecError::MethodResolveError( MethodResolveError::MethodInvalidSignature )) .to_string(), "MethodInvalidSignature" ); assert_eq!( - VMError::FunctionCallError(FunctionCallError::CompilationError( + VMError::FunctionExecError(FunctionExecError::CompilationError( CompilationError::PrepareError(PrepareError::StackHeightInstrumentation) )) .to_string(), diff --git a/runtime/near-vm-logic/src/logic.rs b/runtime/near-vm-logic/src/logic.rs index 6d4197ae030..a85cdc8818c 100644 --- a/runtime/near-vm-logic/src/logic.rs +++ b/runtime/near-vm-logic/src/logic.rs @@ -203,7 +203,7 @@ impl<'a> VMLogic<'a> { self.gas_counter.pay_per_byte(read_register_byte, data.len() as _)?; Ok(data.clone()) } else { - Err(HostError::InvalidRegisterId(register_id).into()) + Err(HostError::InvalidRegisterId { register_id }.into()) } } @@ -424,7 +424,10 @@ impl<'a> VMLogic<'a> { self.gas_counter.pay_base(base)?; if self.context.is_view { - return Err(HostError::ProhibitedInView("signer_account_id".to_string()).into()); + return Err(HostError::ProhibitedInView { + method_name: "signer_account_id".to_string(), + } + .into()); } self.internal_write_register( register_id, @@ -448,7 +451,10 @@ impl<'a> VMLogic<'a> { self.gas_counter.pay_base(base)?; if self.context.is_view { - return Err(HostError::ProhibitedInView("signer_account_pk".to_string()).into()); + return Err(HostError::ProhibitedInView { + method_name: "signer_account_pk".to_string(), + } + .into()); } self.internal_write_register(register_id, self.context.signer_account_pk.clone()) } @@ -469,7 +475,10 @@ impl<'a> VMLogic<'a> { self.gas_counter.pay_base(base)?; if self.context.is_view { - return Err(HostError::ProhibitedInView("predecessor_account_id".to_string()).into()); + return Err(HostError::ProhibitedInView { + method_name: "predecessor_account_id".to_string(), + } + .into()); } self.internal_write_register( register_id, @@ -565,7 +574,10 @@ impl<'a> VMLogic<'a> { self.gas_counter.pay_base(base)?; if self.context.is_view { - return Err(HostError::ProhibitedInView("attached_deposit".to_string()).into()); + return Err(HostError::ProhibitedInView { + method_name: "attached_deposit".to_string(), + } + .into()); } self.memory_set_u128(balance_ptr, self.context.attached_deposit) } @@ -582,7 +594,9 @@ impl<'a> VMLogic<'a> { pub fn prepaid_gas(&mut self) -> Result { self.gas_counter.pay_base(base)?; if self.context.is_view { - return Err(HostError::ProhibitedInView("prepaid_gas".to_string()).into()); + return Err( + HostError::ProhibitedInView { method_name: "prepaid_gas".to_string() }.into() + ); } Ok(self.context.prepaid_gas) } @@ -599,7 +613,7 @@ impl<'a> VMLogic<'a> { pub fn used_gas(&mut self) -> Result { self.gas_counter.pay_base(base)?; if self.context.is_view { - return Err(HostError::ProhibitedInView("used_gas".to_string()).into()); + return Err(HostError::ProhibitedInView { method_name: "used_gas".to_string() }.into()); } Ok(self.gas_counter.used_gas()) } @@ -811,7 +825,9 @@ impl<'a> VMLogic<'a> { ) -> Result { self.gas_counter.pay_base(base)?; if self.context.is_view { - return Err(HostError::ProhibitedInView("promise_and".to_string()).into()); + return Err( + HostError::ProhibitedInView { method_name: "promise_and".to_string() }.into() + ); } self.gas_counter.pay_base(promise_and_base)?; self.gas_counter.pay_per_byte( @@ -828,7 +844,7 @@ impl<'a> VMLogic<'a> { let promise = self .promises .get(*promise_idx as usize) - .ok_or(HostError::InvalidPromiseIndex(*promise_idx))?; + .ok_or(HostError::InvalidPromiseIndex { promise_idx: *promise_idx })?; match &promise { Promise::Receipt(receipt_idx) => { receipt_dependencies.push(*receipt_idx); @@ -867,7 +883,10 @@ impl<'a> VMLogic<'a> { ) -> Result { self.gas_counter.pay_base(base)?; if self.context.is_view { - return Err(HostError::ProhibitedInView("promise_batch_create".to_string()).into()); + return Err(HostError::ProhibitedInView { + method_name: "promise_batch_create".to_string(), + } + .into()); } let account_id = self.read_and_parse_account_id(account_id_ptr, account_id_len)?; let sir = account_id == self.context.current_account_id; @@ -907,14 +926,17 @@ impl<'a> VMLogic<'a> { ) -> Result { self.gas_counter.pay_base(base)?; if self.context.is_view { - return Err(HostError::ProhibitedInView("promise_batch_then".to_string()).into()); + return Err(HostError::ProhibitedInView { + method_name: "promise_batch_then".to_string(), + } + .into()); } let account_id = self.read_and_parse_account_id(account_id_ptr, account_id_len)?; // Update the DAG and return new promise idx. let promise = self .promises .get(promise_idx as usize) - .ok_or(HostError::InvalidPromiseIndex(promise_idx))?; + .ok_or(HostError::InvalidPromiseIndex { promise_idx })?; let receipt_dependencies = match &promise { Promise::Receipt(receipt_idx) => vec![*receipt_idx], Promise::NotReceipt(receipt_indices) => receipt_indices.clone(), @@ -949,7 +971,7 @@ impl<'a> VMLogic<'a> { let promise = self .promises .get(promise_idx as usize) - .ok_or(HostError::InvalidPromiseIndex(promise_idx))?; + .ok_or(HostError::InvalidPromiseIndex { promise_idx })?; let receipt_idx = match &promise { Promise::Receipt(receipt_idx) => Ok(*receipt_idx), Promise::NotReceipt(_) => Err(HostError::CannotAppendActionToJointPromise), @@ -980,9 +1002,9 @@ impl<'a> VMLogic<'a> { pub fn promise_batch_action_create_account(&mut self, promise_idx: u64) -> Result<()> { self.gas_counter.pay_base(base)?; if self.context.is_view { - return Err(HostError::ProhibitedInView( - "promise_batch_action_create_account".to_string(), - ) + return Err(HostError::ProhibitedInView { + method_name: "promise_batch_action_create_account".to_string(), + } .into()); } let (receipt_idx, sir) = self.promise_idx_to_receipt_idx_with_sir(promise_idx)?; @@ -1018,9 +1040,9 @@ impl<'a> VMLogic<'a> { ) -> Result<()> { self.gas_counter.pay_base(base)?; if self.context.is_view { - return Err(HostError::ProhibitedInView( - "promise_batch_action_deploy_contract".to_string(), - ) + return Err(HostError::ProhibitedInView { + method_name: "promise_batch_action_deploy_contract".to_string(), + } .into()); } let code = self.get_vec_from_memory_or_register(code_ptr, code_len)?; @@ -1070,9 +1092,9 @@ impl<'a> VMLogic<'a> { ) -> Result<()> { self.gas_counter.pay_base(base)?; if self.context.is_view { - return Err(HostError::ProhibitedInView( - "promise_batch_action_function_call".to_string(), - ) + return Err(HostError::ProhibitedInView { + method_name: "promise_batch_action_function_call".to_string(), + } .into()); } let amount = self.memory_get_u128(amount_ptr)?; @@ -1124,9 +1146,10 @@ impl<'a> VMLogic<'a> { ) -> Result<()> { self.gas_counter.pay_base(base)?; if self.context.is_view { - return Err( - HostError::ProhibitedInView("promise_batch_action_transfer".to_string()).into() - ); + return Err(HostError::ProhibitedInView { + method_name: "promise_batch_action_transfer".to_string(), + } + .into()); } let amount = self.memory_get_u128(amount_ptr)?; @@ -1167,9 +1190,10 @@ impl<'a> VMLogic<'a> { ) -> Result<()> { self.gas_counter.pay_base(base)?; if self.context.is_view { - return Err( - HostError::ProhibitedInView("promise_batch_action_stake".to_string()).into() - ); + return Err(HostError::ProhibitedInView { + method_name: "promise_batch_action_stake".to_string(), + } + .into()); } let amount = self.memory_get_u128(amount_ptr)?; let public_key = self.get_vec_from_memory_or_register(public_key_ptr, public_key_len)?; @@ -1211,9 +1235,9 @@ impl<'a> VMLogic<'a> { ) -> Result<()> { self.gas_counter.pay_base(base)?; if self.context.is_view { - return Err(HostError::ProhibitedInView( - "promise_batch_action_add_key_with_full_access".to_string(), - ) + return Err(HostError::ProhibitedInView { + method_name: "promise_batch_action_add_key_with_full_access".to_string(), + } .into()); } let public_key = self.get_vec_from_memory_or_register(public_key_ptr, public_key_len)?; @@ -1262,9 +1286,9 @@ impl<'a> VMLogic<'a> { ) -> Result<()> { self.gas_counter.pay_base(base)?; if self.context.is_view { - return Err(HostError::ProhibitedInView( - "promise_batch_action_add_key_with_function_call".to_string(), - ) + return Err(HostError::ProhibitedInView { + method_name: "promise_batch_action_add_key_with_function_call".to_string(), + } .into()); } let public_key = self.get_vec_from_memory_or_register(public_key_ptr, public_key_len)?; @@ -1336,9 +1360,10 @@ impl<'a> VMLogic<'a> { ) -> Result<()> { self.gas_counter.pay_base(base)?; if self.context.is_view { - return Err( - HostError::ProhibitedInView("promise_batch_action_delete_key".to_string()).into() - ); + return Err(HostError::ProhibitedInView { + method_name: "promise_batch_action_delete_key".to_string(), + } + .into()); } let public_key = self.get_vec_from_memory_or_register(public_key_ptr, public_key_len)?; @@ -1375,9 +1400,9 @@ impl<'a> VMLogic<'a> { ) -> Result<()> { self.gas_counter.pay_base(base)?; if self.context.is_view { - return Err(HostError::ProhibitedInView( - "promise_batch_action_delete_account".to_string(), - ) + return Err(HostError::ProhibitedInView { + method_name: "promise_batch_action_delete_account".to_string(), + } .into()); } let beneficiary_id = @@ -1409,7 +1434,10 @@ impl<'a> VMLogic<'a> { pub fn promise_results_count(&mut self) -> Result { self.gas_counter.pay_base(base)?; if self.context.is_view { - return Err(HostError::ProhibitedInView("promise_results_count".to_string()).into()); + return Err(HostError::ProhibitedInView { + method_name: "promise_results_count".to_string(), + } + .into()); } Ok(self.promise_results.len() as _) } @@ -1439,12 +1467,14 @@ impl<'a> VMLogic<'a> { pub fn promise_result(&mut self, result_idx: u64, register_id: u64) -> Result { self.gas_counter.pay_base(base)?; if self.context.is_view { - return Err(HostError::ProhibitedInView("promise_result".to_string()).into()); + return Err( + HostError::ProhibitedInView { method_name: "promise_result".to_string() }.into() + ); } match self .promise_results .get(result_idx as usize) - .ok_or(HostError::InvalidPromiseResultIndex(result_idx))? + .ok_or(HostError::InvalidPromiseResultIndex { result_idx })? { PromiseResult::NotReady => Ok(0), PromiseResult::Successful(data) => { @@ -1470,12 +1500,14 @@ impl<'a> VMLogic<'a> { self.gas_counter.pay_base(base)?; self.gas_counter.pay_base(promise_return)?; if self.context.is_view { - return Err(HostError::ProhibitedInView("promise_return".to_string()).into()); + return Err( + HostError::ProhibitedInView { method_name: "promise_return".to_string() }.into() + ); } match self .promises .get(promise_idx as usize) - .ok_or(HostError::InvalidPromiseIndex(promise_idx))? + .ok_or(HostError::InvalidPromiseIndex { promise_idx })? { Promise::Receipt(receipt_idx) => { self.return_data = ReturnData::ReceiptIndex(*receipt_idx); @@ -1537,7 +1569,7 @@ impl<'a> VMLogic<'a> { /// `base` pub fn panic(&mut self) -> Result<()> { self.gas_counter.pay_base(base)?; - Err(HostError::GuestPanic("explicit guest panic".to_string()).into()) + Err(HostError::GuestPanic { panic_msg: "explicit guest panic".to_string() }.into()) } /// Guest panics with the UTF-8 encoded string. @@ -1553,7 +1585,7 @@ impl<'a> VMLogic<'a> { /// `base + cost of reading and decoding a utf8 string` pub fn panic_utf8(&mut self, len: u64, ptr: u64) -> Result<()> { self.gas_counter.pay_base(base)?; - Err(HostError::GuestPanic(self.get_utf8_string(len, ptr)?).into()) + Err(HostError::GuestPanic { panic_msg: self.get_utf8_string(len, ptr)? }.into()) } /// Logs the UTF-8 encoded string. @@ -1623,7 +1655,7 @@ impl<'a> VMLogic<'a> { self.gas_counter.pay_per_byte(log_byte, message.as_bytes().len() as u64)?; self.logs.push(format!("ABORT: {}", message)); - Err(HostError::GuestPanic(message).into()) + Err(HostError::GuestPanic { panic_msg: message }.into()) } // ############### @@ -1937,9 +1969,9 @@ impl<'a> VMLogic<'a> { self.gas_counter.pay_base(base)?; self.gas_counter.pay_base(storage_iter_next_base)?; if self.invalid_iterators.contains(&iterator_id) { - return Err(HostError::IteratorWasInvalidated(iterator_id).into()); + return Err(HostError::IteratorWasInvalidated { iterator_index: iterator_id }.into()); } else if !self.valid_iterators.contains(&iterator_id) { - return Err(HostError::InvalidIteratorIndex(iterator_id).into()); + return Err(HostError::InvalidIteratorIndex { iterator_index: iterator_id }.into()); } let nodes_before = self.ext.get_touched_nodes_count(); diff --git a/runtime/near-vm-logic/src/mocks/mock_external.rs b/runtime/near-vm-logic/src/mocks/mock_external.rs index 60ccbc9c367..6df3a098c21 100644 --- a/runtime/near-vm-logic/src/mocks/mock_external.rs +++ b/runtime/near-vm-logic/src/mocks/mock_external.rs @@ -123,13 +123,13 @@ impl External for MockedExternal { } None => Ok(None), }, - None => Err(HostError::InvalidIteratorIndex(iterator_idx).into()), + None => Err(HostError::InvalidIteratorIndex { iterator_index: iterator_idx }.into()), } } fn storage_iter_drop(&mut self, iterator_idx: u64) -> Result<()> { if self.iterators.remove(&iterator_idx).is_none() { - Err(HostError::InvalidIteratorIndex(iterator_idx).into()) + Err(HostError::InvalidIteratorIndex { iterator_index: iterator_idx }.into()) } else { Ok(()) } @@ -137,7 +137,7 @@ impl External for MockedExternal { fn create_receipt(&mut self, receipt_indices: Vec, receiver_id: String) -> Result { if let Some(index) = receipt_indices.iter().find(|&&el| el >= self.receipts.len() as u64) { - return Err(HostError::InvalidReceiptIndex(*index).into()); + return Err(HostError::InvalidReceiptIndex { receipt_index: *index }.into()); } let res = self.receipts.len() as u64; self.receipts.push(Receipt { receipt_indices, receiver_id, actions: vec![] }); diff --git a/runtime/near-vm-logic/tests/test_registers.rs b/runtime/near-vm-logic/tests/test_registers.rs index ddc2cc132cd..7e69cf63491 100644 --- a/runtime/near-vm-logic/tests/test_registers.rs +++ b/runtime/near-vm-logic/tests/test_registers.rs @@ -26,7 +26,7 @@ fn test_non_existent_register() { let buffer = [0u8; 3]; assert_eq!( logic.read_register(0, buffer.as_ptr() as u64), - Err(HostError::InvalidRegisterId(0).into()) + Err(HostError::InvalidRegisterId { register_id: 0 }.into()) ); } diff --git a/runtime/near-vm-runner/src/errors.rs b/runtime/near-vm-runner/src/errors.rs index 34b604c76af..76d96eaacc6 100644 --- a/runtime/near-vm-runner/src/errors.rs +++ b/runtime/near-vm-runner/src/errors.rs @@ -1,4 +1,4 @@ -use near_vm_errors::{CompilationError, FunctionCallError, MethodResolveError, VMError}; +use near_vm_errors::{CompilationError, FunctionExecError, MethodResolveError, VMError}; use near_vm_logic::HostErrorOrStorageError; pub trait IntoVMError { @@ -10,9 +10,9 @@ impl IntoVMError for wasmer_runtime::error::Error { use wasmer_runtime::error::Error; match self { Error::CompileError(err) => err.into_vm_error(), - Error::LinkError(err) => VMError::FunctionCallError(FunctionCallError::LinkError( - format!("{:.500}", Error::LinkError(err).to_string()), - )), + Error::LinkError(err) => VMError::FunctionExecError(FunctionExecError::LinkError { + msg: format!("{:.500}", Error::LinkError(err).to_string()), + }), Error::RuntimeError(err) => err.into_vm_error(), Error::ResolveError(err) => err.into_vm_error(), Error::CallError(err) => err.into_vm_error(), @@ -33,8 +33,8 @@ impl IntoVMError for wasmer_runtime::error::CallError { impl IntoVMError for wasmer_runtime::error::CompileError { fn into_vm_error(self) -> VMError { - VMError::FunctionCallError(FunctionCallError::CompilationError( - CompilationError::WasmerCompileError(self.to_string()), + VMError::FunctionExecError(FunctionExecError::CompilationError( + CompilationError::WasmerCompileError { msg: self.to_string() }, )) } } @@ -43,14 +43,14 @@ impl IntoVMError for wasmer_runtime::error::ResolveError { fn into_vm_error(self) -> VMError { use wasmer_runtime::error::ResolveError as WasmerResolveError; match self { - WasmerResolveError::Signature { .. } => VMError::FunctionCallError( - FunctionCallError::ResolveError(MethodResolveError::MethodInvalidSignature), + WasmerResolveError::Signature { .. } => VMError::FunctionExecError( + FunctionExecError::MethodResolveError(MethodResolveError::MethodInvalidSignature), ), - WasmerResolveError::ExportNotFound { .. } => VMError::FunctionCallError( - FunctionCallError::ResolveError(MethodResolveError::MethodNotFound), + WasmerResolveError::ExportNotFound { .. } => VMError::FunctionExecError( + FunctionExecError::MethodResolveError(MethodResolveError::MethodNotFound), ), - WasmerResolveError::ExportWrongType { .. } => VMError::FunctionCallError( - FunctionCallError::ResolveError(MethodResolveError::MethodNotFound), + WasmerResolveError::ExportWrongType { .. } => VMError::FunctionExecError( + FunctionExecError::MethodResolveError(MethodResolveError::MethodNotFound), ), } } @@ -61,7 +61,7 @@ impl IntoVMError for wasmer_runtime::error::RuntimeError { use wasmer_runtime::error::RuntimeError; match &self { RuntimeError::Trap { msg } => { - VMError::FunctionCallError(FunctionCallError::WasmTrap(msg.to_string())) + VMError::FunctionExecError(FunctionExecError::WasmTrap { msg: msg.to_string() }) } RuntimeError::Error { data } => { if let Some(err) = data.downcast_ref::() { @@ -70,7 +70,7 @@ impl IntoVMError for wasmer_runtime::error::RuntimeError { VMError::StorageError(s.clone()) } HostErrorOrStorageError::HostError(h) => { - VMError::FunctionCallError(FunctionCallError::HostError(h.clone())) + VMError::FunctionExecError(FunctionExecError::HostError(h.clone())) } } } else { @@ -79,7 +79,9 @@ impl IntoVMError for wasmer_runtime::error::RuntimeError { data.type_id(), self.to_string() ); - VMError::FunctionCallError(FunctionCallError::WasmTrap("unknown".to_string())) + VMError::FunctionExecError(FunctionExecError::WasmTrap { + msg: "unknown".to_string(), + }) } } } diff --git a/runtime/near-vm-runner/src/runner.rs b/runtime/near-vm-runner/src/runner.rs index 360a1001c39..be724759793 100644 --- a/runtime/near-vm-runner/src/runner.rs +++ b/runtime/near-vm-runner/src/runner.rs @@ -2,7 +2,7 @@ use crate::errors::IntoVMError; use crate::memory::WasmerMemory; use crate::{cache, imports}; use near_runtime_fees::RuntimeFeesConfig; -use near_vm_errors::{FunctionCallError, MethodResolveError, VMError}; +use near_vm_errors::{FunctionExecError, MethodResolveError, VMError}; use near_vm_logic::types::PromiseResult; use near_vm_logic::{External, VMConfig, VMContext, VMLogic, VMOutcome}; use wasmer_runtime::Module; @@ -16,12 +16,12 @@ fn check_method(module: &Module, method_name: &str) -> Result<(), VMError> { if sig.params().is_empty() && sig.returns().is_empty() { Ok(()) } else { - Err(VMError::FunctionCallError(FunctionCallError::ResolveError( + Err(VMError::FunctionExecError(FunctionExecError::MethodResolveError( MethodResolveError::MethodInvalidSignature, ))) } } else { - Err(VMError::FunctionCallError(FunctionCallError::ResolveError( + Err(VMError::FunctionExecError(FunctionExecError::MethodResolveError( MethodResolveError::MethodNotFound, ))) } @@ -57,7 +57,7 @@ pub fn run<'a>( if method_name.is_empty() { return ( None, - Some(VMError::FunctionCallError(FunctionCallError::ResolveError( + Some(VMError::FunctionExecError(FunctionExecError::MethodResolveError( MethodResolveError::MethodEmptyName, ))), ); @@ -83,7 +83,7 @@ pub fn run<'a>( Err(_) => { return ( None, - Some(VMError::FunctionCallError(FunctionCallError::ResolveError( + Some(VMError::FunctionExecError(FunctionExecError::MethodResolveError( MethodResolveError::MethodUTF8Error, ))), ) diff --git a/runtime/near-vm-runner/tests/test_error_cases.rs b/runtime/near-vm-runner/tests/test_error_cases.rs index c8d5577b719..cff219058b0 100644 --- a/runtime/near-vm-runner/tests/test_error_cases.rs +++ b/runtime/near-vm-runner/tests/test_error_cases.rs @@ -1,5 +1,5 @@ use crate::utils::{make_simple_contract_call, make_simple_contract_call_with_gas}; -use near_vm_errors::{CompilationError, FunctionCallError, MethodResolveError, PrepareError}; +use near_vm_errors::{CompilationError, FunctionExecError, MethodResolveError, PrepareError}; use near_vm_logic::{HostError, ReturnData, VMOutcome}; use near_vm_runner::VMError; @@ -36,7 +36,7 @@ fn test_infinite_initializer() { make_simple_contract_call(&infinite_initializer_contract(), b"hello"), ( Some(vm_outcome_with_gas(100000000000000)), - Some(VMError::FunctionCallError(FunctionCallError::HostError(HostError::GasExceeded))) + Some(VMError::FunctionExecError(FunctionExecError::HostError(HostError::GasExceeded))) ) ); } @@ -47,7 +47,7 @@ fn test_infinite_initializer_export_not_found() { make_simple_contract_call(&infinite_initializer_contract(), b"hello2"), ( None, - Some(VMError::FunctionCallError(FunctionCallError::ResolveError( + Some(VMError::FunctionExecError(FunctionExecError::MethodResolveError( MethodResolveError::MethodNotFound ))) ) @@ -80,7 +80,7 @@ fn test_export_not_found() { make_simple_contract_call(&simple_contract(), b"hello2"), ( None, - Some(VMError::FunctionCallError(FunctionCallError::ResolveError( + Some(VMError::FunctionExecError(FunctionExecError::MethodResolveError( MethodResolveError::MethodNotFound ))) ) @@ -93,7 +93,7 @@ fn test_empty_method() { make_simple_contract_call(&simple_contract(), b""), ( None, - Some(VMError::FunctionCallError(FunctionCallError::ResolveError( + Some(VMError::FunctionExecError(FunctionExecError::MethodResolveError( MethodResolveError::MethodEmptyName ))) ) @@ -106,7 +106,7 @@ fn test_invalid_utf8() { make_simple_contract_call(&simple_contract(), &[255u8]), ( None, - Some(VMError::FunctionCallError(FunctionCallError::ResolveError( + Some(VMError::FunctionExecError(FunctionExecError::MethodResolveError( MethodResolveError::MethodUTF8Error ))) ) @@ -131,7 +131,9 @@ fn test_trap_contract() { make_simple_contract_call(&trap_contract(), b"hello"), ( Some(vm_outcome_with_gas(3856371)), - Some(VMError::FunctionCallError(FunctionCallError::WasmTrap("unknown".to_string()))) + Some(VMError::FunctionExecError(FunctionExecError::WasmTrap { + msg: "unknown".to_string() + })) ) ); } @@ -155,7 +157,9 @@ fn test_trap_initializer() { make_simple_contract_call(&trap_initializer(), b"hello"), ( Some(vm_outcome_with_gas(3856371)), - Some(VMError::FunctionCallError(FunctionCallError::WasmTrap("unknown".to_string()))) + Some(VMError::FunctionExecError(FunctionExecError::WasmTrap { + msg: "unknown".to_string() + })) ) ); } @@ -178,7 +182,7 @@ fn test_wrong_signature_contract() { make_simple_contract_call(&wrong_signature_contract(), b"hello"), ( None, - Some(VMError::FunctionCallError(FunctionCallError::ResolveError( + Some(VMError::FunctionExecError(FunctionExecError::MethodResolveError( MethodResolveError::MethodInvalidSignature ))) ) @@ -202,7 +206,7 @@ fn test_export_wrong_type() { make_simple_contract_call(&export_wrong_type(), b"hello"), ( None, - Some(VMError::FunctionCallError(FunctionCallError::ResolveError( + Some(VMError::FunctionExecError(FunctionExecError::MethodResolveError( MethodResolveError::MethodNotFound ))) ) @@ -228,9 +232,9 @@ fn test_guest_panic() { make_simple_contract_call(&guest_panic(), b"hello"), ( Some(vm_outcome_with_gas(130080593)), - Some(VMError::FunctionCallError(FunctionCallError::HostError(HostError::GuestPanic( - "explicit guest panic".to_string() - )))) + Some(VMError::FunctionExecError(FunctionExecError::HostError(HostError::GuestPanic { + panic_msg: "explicit guest panic".to_string() + }))) ) ); } @@ -253,7 +257,9 @@ fn test_stack_overflow() { make_simple_contract_call(&stack_overflow(), b"hello"), ( Some(vm_outcome_with_gas(63182782464)), - Some(VMError::FunctionCallError(FunctionCallError::WasmTrap("unknown".to_string()))) + Some(VMError::FunctionExecError(FunctionExecError::WasmTrap { + msg: "unknown".to_string() + })) ) ); } @@ -283,7 +289,7 @@ fn test_memory_grow() { make_simple_contract_call(&memory_grow(), b"hello"), ( Some(vm_outcome_with_gas(100000000000000)), - Some(VMError::FunctionCallError(FunctionCallError::HostError(HostError::GasExceeded))) + Some(VMError::FunctionExecError(FunctionExecError::HostError(HostError::GasExceeded))) ) ); } @@ -325,7 +331,7 @@ fn test_bad_import_1() { make_simple_contract_call(&bad_import_global("wtf"), b"hello"), ( None, - Some(VMError::FunctionCallError(FunctionCallError::CompilationError( + Some(VMError::FunctionExecError(FunctionExecError::CompilationError( CompilationError::PrepareError(PrepareError::Instantiate) ))) ) @@ -338,7 +344,7 @@ fn test_bad_import_2() { make_simple_contract_call(&bad_import_func("wtf"), b"hello"), ( None, - Some(VMError::FunctionCallError(FunctionCallError::CompilationError( + Some(VMError::FunctionExecError(FunctionExecError::CompilationError( CompilationError::PrepareError(PrepareError::Instantiate) ))) ) @@ -351,9 +357,9 @@ fn test_bad_import_3() { make_simple_contract_call(&bad_import_global("env"), b"hello"), ( Some(vm_outcome_with_gas(0)), - Some(VMError::FunctionCallError(FunctionCallError::LinkError( - "link error: Incorrect import type, namespace: env, name: input, expected type: global, found type: function".to_string() - ))) + Some(VMError::FunctionExecError(FunctionExecError::LinkError{ + msg: "link error: Incorrect import type, namespace: env, name: input, expected type: global, found type: function".to_string() + })) ) ); } @@ -364,9 +370,9 @@ fn test_bad_import_4() { make_simple_contract_call(&bad_import_func("env"), b"hello"), ( Some(vm_outcome_with_gas(0)), - Some(VMError::FunctionCallError(FunctionCallError::LinkError( - "link error: Import not found, namespace: env, name: wtf".to_string() - ))) + Some(VMError::FunctionExecError(FunctionExecError::LinkError { + msg: "link error: Import not found, namespace: env, name: wtf".to_string() + })) ) ); } @@ -390,7 +396,7 @@ fn test_initializer_no_gas() { make_simple_contract_call_with_gas(&some_initializer_contract(), b"hello", 0), ( Some(vm_outcome_with_gas(0)), - Some(VMError::FunctionCallError(FunctionCallError::HostError(HostError::GasExceeded))) + Some(VMError::FunctionExecError(FunctionExecError::HostError(HostError::GasExceeded))) ) ); } @@ -421,7 +427,7 @@ fn bad_many_imports() -> Vec { fn test_bad_many_imports() { let result = make_simple_contract_call(&bad_many_imports(), b"hello"); assert_eq!(result.0, Some(vm_outcome_with_gas(0))); - if let Some(VMError::FunctionCallError(FunctionCallError::LinkError(msg))) = result.1 { + if let Some(VMError::FunctionExecError(FunctionExecError::LinkError { msg })) = result.1 { eprintln!("{}", msg); assert!(msg.len() < 1000, format!("Huge error message: {}", msg.len())); } else { diff --git a/runtime/near-vm-runner/tests/test_invalid_contracts.rs b/runtime/near-vm-runner/tests/test_invalid_contracts.rs index acfddb042f7..b20f3bc1f46 100644 --- a/runtime/near-vm-runner/tests/test_invalid_contracts.rs +++ b/runtime/near-vm-runner/tests/test_invalid_contracts.rs @@ -1,5 +1,5 @@ use crate::utils::{make_simple_contract_call, wat2wasm_no_validate}; -use near_vm_errors::{CompilationError, FunctionCallError, PrepareError}; +use near_vm_errors::{CompilationError, FunctionExecError, PrepareError}; use near_vm_runner::VMError; mod utils; @@ -22,7 +22,7 @@ fn test_initializer_wrong_signature_contract() { make_simple_contract_call(&initializer_wrong_signature_contract(), b"hello"), ( None, - Some(VMError::FunctionCallError(FunctionCallError::CompilationError( + Some(VMError::FunctionExecError(FunctionExecError::CompilationError( CompilationError::PrepareError(PrepareError::Deserialization) ))) ) @@ -45,7 +45,7 @@ fn test_function_not_defined_contract() { make_simple_contract_call(&function_not_defined_contract(), b"hello"), ( None, - Some(VMError::FunctionCallError(FunctionCallError::CompilationError( + Some(VMError::FunctionExecError(FunctionExecError::CompilationError( CompilationError::PrepareError(PrepareError::Deserialization) ))) ) @@ -69,7 +69,7 @@ fn test_function_type_not_defined_contract_1() { make_simple_contract_call(&function_type_not_defined_contract(1), b"hello"), ( None, - Some(VMError::FunctionCallError(FunctionCallError::CompilationError( + Some(VMError::FunctionExecError(FunctionExecError::CompilationError( CompilationError::PrepareError(PrepareError::Deserialization) ))) ) @@ -83,7 +83,7 @@ fn test_function_type_not_defined_contract_2() { make_simple_contract_call(&function_type_not_defined_contract(0), b"hello"), ( None, - Some(VMError::FunctionCallError(FunctionCallError::CompilationError( + Some(VMError::FunctionExecError(FunctionExecError::CompilationError( CompilationError::PrepareError(PrepareError::Deserialization) ))) ) @@ -96,7 +96,7 @@ fn test_garbage_contract() { make_simple_contract_call(&[], b"hello"), ( None, - Some(VMError::FunctionCallError(FunctionCallError::CompilationError( + Some(VMError::FunctionExecError(FunctionExecError::CompilationError( CompilationError::PrepareError(PrepareError::Deserialization) ))) ) @@ -121,7 +121,7 @@ fn test_evil_function_index() { make_simple_contract_call(&evil_function_index(), b"abort_with_zero"), ( None, - Some(VMError::FunctionCallError(FunctionCallError::CompilationError( + Some(VMError::FunctionExecError(FunctionExecError::CompilationError( CompilationError::PrepareError(PrepareError::Deserialization) ))) ) diff --git a/runtime/near-vm-runner/tests/test_rs_contract.rs b/runtime/near-vm-runner/tests/test_rs_contract.rs index a46e0756cfb..b3bdf75390f 100644 --- a/runtime/near-vm-runner/tests/test_rs_contract.rs +++ b/runtime/near-vm-runner/tests/test_rs_contract.rs @@ -1,5 +1,5 @@ use near_runtime_fees::RuntimeFeesConfig; -use near_vm_errors::FunctionCallError; +use near_vm_errors::FunctionExecError; use near_vm_logic::mocks::mock_external::MockedExternal; use near_vm_logic::types::ReturnData; use near_vm_logic::{VMConfig, VMContext, VMOutcome}; @@ -174,6 +174,8 @@ pub fn test_out_of_memory() { ); assert_eq!( result.1, - Some(VMError::FunctionCallError(FunctionCallError::WasmTrap("unknown".to_string()))) + Some(VMError::FunctionExecError(FunctionExecError::WasmTrap { + msg: "unknown".to_string() + })) ); } diff --git a/runtime/near-vm-runner/tests/test_ts_contract.rs b/runtime/near-vm-runner/tests/test_ts_contract.rs index c3b2c576c3b..88c93fe4581 100644 --- a/runtime/near-vm-runner/tests/test_ts_contract.rs +++ b/runtime/near-vm-runner/tests/test_ts_contract.rs @@ -1,5 +1,5 @@ use near_runtime_fees::RuntimeFeesConfig; -use near_vm_errors::FunctionCallError; +use near_vm_errors::FunctionExecError; use near_vm_logic::mocks::mock_external::MockedExternal; use near_vm_logic::types::ReturnData; use near_vm_logic::{External, HostError, VMConfig, VMContext}; @@ -37,9 +37,9 @@ pub fn test_ts_contract() { ); assert_eq!( result.1, - Some(VMError::FunctionCallError(FunctionCallError::HostError(HostError::GuestPanic( - "explicit guest panic".to_string() - )))) + Some(VMError::FunctionExecError(FunctionExecError::HostError(HostError::GuestPanic { + panic_msg: "explicit guest panic".to_string() + }))) ); // Call method that writes something into storage. diff --git a/runtime/runtime/Cargo.toml b/runtime/runtime/Cargo.toml index fe582aff2a2..6ba44a6d218 100644 --- a/runtime/runtime/Cargo.toml +++ b/runtime/runtime/Cargo.toml @@ -30,6 +30,7 @@ near-vm-errors = { path = "../../runtime/near-vm-errors" } [features] default = [] +dump_errors_schema = ["near-vm-errors/dump_errors_schema"] # Use this feature to enable counting of fees and costs applied. costs_counting = ["near-vm-logic/costs_counting", "near-vm-runner/costs_counting"] diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index 6be9fba1ab0..d83517238da 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -26,8 +26,8 @@ use near_vm_logic::VMContext; use crate::config::RuntimeConfig; use crate::ext::RuntimeExt; use crate::{ActionResult, ApplyState}; -use near_primitives::errors::ActionError; -use near_vm_errors::{CompilationError, FunctionCallError}; +use near_primitives::errors::{ActionError, ActionErrorKind}; +use near_vm_errors::{CompilationError, FunctionExecError}; use near_vm_runner::VMError; /// Number of epochs it takes to unstake. @@ -120,10 +120,10 @@ pub(crate) fn action_function_call( let code = match get_code_with_cache(state_update, account_id, &account) { Ok(Some(code)) => code, Ok(None) => { - let error = FunctionCallError::CompilationError(CompilationError::CodeDoesNotExist( - account_id.clone(), + let error = VMError::FunctionExecError(FunctionExecError::CompilationError( + CompilationError::CodeDoesNotExist { account_id: account_id.clone() }, )); - result.result = Err(ActionError::FunctionCallError(error.to_string())); + result.result = Err(ActionErrorKind::FunctionCall(error).into()); return Ok(()); } Err(e) => { @@ -181,8 +181,7 @@ pub(crate) fn action_function_call( borsh::BorshDeserialize::try_from_slice(&storage).expect("Borsh cannot fail"); return Err(err); } - // TODO(#1731): Handle VMError::FunctionCallError better. - result.result = Err(ActionError::FunctionCallError(err.to_string())); + result.result = Err(ActionErrorKind::FunctionCall(err).into()); if let Some(outcome) = outcome { result.gas_burnt += outcome.burnt_gas; result.gas_burnt_for_function_call += outcome.burnt_gas; @@ -218,7 +217,8 @@ pub(crate) fn action_stake( if account.amount >= increment { if account.locked == 0 && stake.stake == 0 { // if the account hasn't staked, it cannot unstake - result.result = Err(ActionError::TriesToUnstake(account_id.clone())); + result.result = + Err(ActionErrorKind::TriesToUnstake { account_id: account_id.clone() }.into()); return; } result.validator_proposals.push(ValidatorStake { @@ -231,12 +231,13 @@ pub(crate) fn action_stake( account.locked = stake.stake; } } else { - result.result = Err(ActionError::TriesToStake( - account_id.clone(), - stake.stake, - account.locked, - account.amount, - )); + result.result = Err(ActionErrorKind::TriesToStake { + account_id: account_id.clone(), + stake: stake.stake, + locked: account.locked, + balance: account.amount, + } + .into()); } } @@ -256,10 +257,11 @@ pub(crate) fn action_create_account( if !is_valid_top_level_account_id(account_id) && !is_valid_sub_account_id(&receipt.predecessor_id, account_id) { - result.result = Err(ActionError::CreateAccountNotAllowed( - account_id.clone(), - receipt.predecessor_id.clone(), - )); + result.result = Err(ActionErrorKind::CreateAccountNotAllowed { + account_id: account_id.clone(), + predecessor_id: receipt.predecessor_id.clone(), + } + .into()); return; } *actor_id = receipt.receiver_id.clone(); @@ -322,7 +324,11 @@ pub(crate) fn action_delete_key( let account = account.as_mut().unwrap(); let access_key = get_access_key(state_update, account_id, &delete_key.public_key)?; if access_key.is_none() { - result.result = Err(ActionError::DeleteKeyDoesNotExist(account_id.clone())); + result.result = Err(ActionErrorKind::DeleteKeyDoesNotExist { + public_key: delete_key.public_key.clone(), + account_id: account_id.clone(), + } + .into()); return Ok(()); } // Remove access key @@ -349,7 +355,11 @@ pub(crate) fn action_add_key( ) -> Result<(), StorageError> { let account = account.as_mut().unwrap(); if get_access_key(state_update, account_id, &add_key.public_key)?.is_some() { - result.result = Err(ActionError::AddKeyAlreadyExists(add_key.public_key.clone())); + result.result = Err(ActionErrorKind::AddKeyAlreadyExists { + account_id: account_id.to_owned(), + public_key: add_key.public_key.clone(), + } + .into()); return Ok(()); } set_access_key(state_update, account_id, &add_key.public_key, &add_key.access_key); @@ -375,16 +385,19 @@ pub(crate) fn check_actor_permissions( match action { Action::DeployContract(_) | Action::Stake(_) | Action::AddKey(_) | Action::DeleteKey(_) => { if actor_id != account_id { - return Err(ActionError::ActorNoPermission( - actor_id.clone(), - account_id.clone(), - action_type_as_string(action).to_owned(), - )); + return Err(ActionErrorKind::ActorNoPermission { + account_id: actor_id.clone(), + actor_id: account_id.clone(), + } + .into()); } } Action::DeleteAccount(_) => { if account.as_ref().unwrap().locked != 0 { - return Err(ActionError::DeleteAccountStaking(account_id.clone())); + return Err(ActionErrorKind::DeleteAccountStaking { + account_id: account_id.clone(), + } + .into()); } if actor_id != account_id && check_rent( @@ -395,10 +408,11 @@ pub(crate) fn check_actor_permissions( ) .is_ok() { - return Err(ActionError::DeleteAccountHasRent( - account_id.clone(), - account.as_ref().unwrap().amount, - )); + return Err(ActionErrorKind::DeleteAccountHasRent { + account_id: account_id.clone(), + balance: account.as_ref().unwrap().amount, + } + .into()); } } Action::CreateAccount(_) | Action::FunctionCall(_) | Action::Transfer(_) => (), @@ -406,19 +420,6 @@ pub(crate) fn check_actor_permissions( Ok(()) } -fn action_type_as_string(action: &Action) -> &'static str { - match action { - Action::CreateAccount(_) => "CreateAccount", - Action::DeployContract(_) => "DeployContract", - Action::FunctionCall(_) => "FunctionCall", - Action::Transfer(_) => "Transfer", - Action::Stake(_) => "Stake", - Action::AddKey(_) => "AddKey", - Action::DeleteKey(_) => "DeleteKey", - Action::DeleteAccount(_) => "DeleteAccount", - } -} - pub(crate) fn check_account_existence( action: &Action, account: &mut Option, @@ -427,7 +428,10 @@ pub(crate) fn check_account_existence( match action { Action::CreateAccount(_) => { if account.is_some() { - return Err(ActionError::AccountAlreadyExists(account_id.clone())); + return Err(ActionErrorKind::AccountAlreadyExists { + account_id: account_id.clone().into(), + } + .into()); } } Action::DeployContract(_) @@ -438,12 +442,11 @@ pub(crate) fn check_account_existence( | Action::DeleteKey(_) | Action::DeleteAccount(_) => { if account.is_none() { - return Err(ActionError::AccountDoesNotExist( - action_type_as_string(action).to_owned(), - account_id.clone(), - )); + return Err(ActionErrorKind::AccountDoesNotExist { + account_id: account_id.clone(), + } + .into()); } - // } }; Ok(()) diff --git a/runtime/runtime/src/balance_checker.rs b/runtime/runtime/src/balance_checker.rs index 22fbd2a0948..083b37c1962 100644 --- a/runtime/runtime/src/balance_checker.rs +++ b/runtime/runtime/src/balance_checker.rs @@ -278,7 +278,7 @@ mod tests { &ApplyStats::default(), ) .unwrap_err(); - assert_matches!(err, RuntimeError::BalanceMismatch(_)); + assert_matches!(err, RuntimeError::BalanceMismatchError(_)); } #[test] diff --git a/runtime/runtime/src/ext.rs b/runtime/runtime/src/ext.rs index d5a130d8bad..f5ce3a91b8f 100644 --- a/runtime/runtime/src/ext.rs +++ b/runtime/runtime/src/ext.rs @@ -166,7 +166,9 @@ impl<'a> External for RuntimeExt<'a> { ) -> ExtResult, Box)>> { let result = match self.iters.get_mut(&iterator_idx) { Some(iter) => iter.next(), - None => return Err(HostError::InvalidIteratorIndex(iterator_idx).into()), + None => { + return Err(HostError::InvalidIteratorIndex { iterator_index: iterator_idx }.into()) + } }; match result { None => { @@ -199,7 +201,7 @@ impl<'a> External for RuntimeExt<'a> { .get_mut(receipt_index as usize) .ok_or_else(|| { HostErrorOrStorageError::HostError( - HostError::InvalidReceiptIndex(receipt_index).into(), + HostError::InvalidReceiptIndex { receipt_index }.into(), ) })? .1 diff --git a/runtime/runtime/src/lib.rs b/runtime/runtime/src/lib.rs index 6766e0f468a..097f7c89b3d 100644 --- a/runtime/runtime/src/lib.rs +++ b/runtime/runtime/src/lib.rs @@ -16,7 +16,8 @@ use near_crypto::PublicKey; use near_primitives::account::{AccessKey, AccessKeyPermission, Account}; use near_primitives::contract::ContractCode; use near_primitives::errors::{ - ActionError, ExecutionError, InvalidAccessKeyError, InvalidTxError, RuntimeError, + ActionError, ActionErrorKind, InvalidAccessKeyError, InvalidTxError, RuntimeError, + TxExecutionError, }; use near_primitives::hash::CryptoHash; use near_primitives::receipt::{ActionReceipt, DataReceipt, Receipt, ReceiptEnum, ReceivedData}; @@ -218,10 +219,13 @@ impl Runtime { let transaction = &signed_transaction.transaction; let signer_id = &transaction.signer_id; if !is_valid_account_id(&signer_id) { - return Err(InvalidTxError::InvalidSigner(signer_id.clone()).into()); + return Err(InvalidTxError::InvalidSignerId { signer_id: signer_id.clone() }.into()); } if !is_valid_account_id(&transaction.receiver_id) { - return Err(InvalidTxError::InvalidReceiver(transaction.receiver_id.clone()).into()); + return Err(InvalidTxError::InvalidReceiverId { + receiver_id: transaction.receiver_id.clone(), + } + .into()); } if !signed_transaction @@ -233,25 +237,31 @@ impl Runtime { let mut signer = match get_account(state_update, signer_id)? { Some(signer) => signer, None => { - return Err(InvalidTxError::SignerDoesNotExist(signer_id.clone()).into()); + return Err( + InvalidTxError::SignerDoesNotExist { signer_id: signer_id.clone() }.into() + ); } }; let mut access_key = match get_access_key(state_update, &signer_id, &transaction.public_key)? { Some(access_key) => access_key, None => { - return Err(InvalidTxError::InvalidAccessKey( - InvalidAccessKeyError::AccessKeyNotFound( - signer_id.clone(), - transaction.public_key.clone(), - ), + return Err(InvalidTxError::InvalidAccessKeyError( + InvalidAccessKeyError::AccessKeyNotFound { + account_id: signer_id.clone(), + public_key: transaction.public_key.clone(), + }, ) .into()); } }; if transaction.nonce <= access_key.nonce { - return Err(InvalidTxError::InvalidNonce(transaction.nonce, access_key.nonce).into()); + return Err(InvalidTxError::InvalidNonce { + tx_nonce: transaction.nonce, + ak_nonce: access_key.nonce, + } + .into()); } let sender_is_receiver = &transaction.receiver_id == signer_id; @@ -268,7 +278,11 @@ impl Runtime { .map_err(|_| InvalidTxError::CostOverflow)?; signer.amount = signer.amount.checked_sub(total_cost).ok_or_else(|| { - InvalidTxError::NotEnoughBalance(signer_id.clone(), signer.amount, total_cost) + InvalidTxError::NotEnoughBalance { + signer_id: signer_id.clone(), + balance: signer.amount, + cost: total_cost, + } })?; if let AccessKeyPermission::FunctionCall(ref mut function_call_permission) = @@ -276,36 +290,39 @@ impl Runtime { { if let Some(ref mut allowance) = function_call_permission.allowance { *allowance = allowance.checked_sub(total_cost).ok_or_else(|| { - InvalidTxError::InvalidAccessKey(InvalidAccessKeyError::NotEnoughAllowance( - signer_id.clone(), - transaction.public_key.clone(), - *allowance, - total_cost, - )) + InvalidTxError::InvalidAccessKeyError( + InvalidAccessKeyError::NotEnoughAllowance { + account_id: signer_id.clone(), + public_key: transaction.public_key.clone(), + allowance: *allowance, + cost: total_cost, + }, + ) })?; } } if let Err(amount) = check_rent(&signer_id, &signer, &self.config, apply_state.epoch_length) { - return Err(InvalidTxError::RentUnpaid(signer_id.clone(), amount).into()); + return Err(InvalidTxError::RentUnpaid { signer_id: signer_id.clone(), amount }.into()); } if let AccessKeyPermission::FunctionCall(ref function_call_permission) = access_key.permission { if transaction.actions.len() != 1 { - return Err( - InvalidTxError::InvalidAccessKey(InvalidAccessKeyError::ActionError).into() - ); + return Err(InvalidTxError::InvalidAccessKeyError( + InvalidAccessKeyError::RequiresFullAccess, + ) + .into()); } if let Some(Action::FunctionCall(ref function_call)) = transaction.actions.get(0) { if transaction.receiver_id != function_call_permission.receiver_id { - return Err(InvalidTxError::InvalidAccessKey( - InvalidAccessKeyError::ReceiverMismatch( - transaction.receiver_id.clone(), - function_call_permission.receiver_id.clone(), - ), + return Err(InvalidTxError::InvalidAccessKeyError( + InvalidAccessKeyError::ReceiverMismatch { + tx_receiver: transaction.receiver_id.clone(), + ak_receiver: function_call_permission.receiver_id.clone(), + }, ) .into()); } @@ -315,17 +332,18 @@ impl Runtime { .iter() .all(|method_name| &function_call.method_name != method_name) { - return Err(InvalidTxError::InvalidAccessKey( - InvalidAccessKeyError::MethodNameMismatch( - function_call.method_name.clone(), - ), + return Err(InvalidTxError::InvalidAccessKeyError( + InvalidAccessKeyError::MethodNameMismatch { + method_name: function_call.method_name.clone(), + }, ) .into()); } } else { - return Err( - InvalidTxError::InvalidAccessKey(InvalidAccessKeyError::ActionError).into() - ); + return Err(InvalidTxError::InvalidAccessKeyError( + InvalidAccessKeyError::RequiresFullAccess, + ) + .into()); } }; @@ -598,7 +616,8 @@ impl Runtime { is_last_action, )?)?; // TODO storage error - if result.result.is_err() { + if let Err(ref mut res) = result.result { + res.index = Some(action_index as u64); break; } } @@ -610,7 +629,13 @@ impl Runtime { check_rent(account_id, account, &self.config, apply_state.epoch_length) { result.merge(ActionResult { - result: Err(ActionError::RentUnpaid(account_id.clone(), amount)), + result: Err(ActionError { + index: None, + kind: ActionErrorKind::RentUnpaid { + account_id: account_id.clone(), + amount, + }, + }), ..Default::default() })?; } else { @@ -745,7 +770,7 @@ impl Runtime { ), Ok(ReturnData::Value(data)) => ExecutionStatus::SuccessValue(data), Ok(ReturnData::None) => ExecutionStatus::SuccessValue(vec![]), - Err(e) => ExecutionStatus::Failure(ExecutionError::Action(e)), + Err(e) => ExecutionStatus::Failure(TxExecutionError::ActionError(e)), }; Self::print_log(&result.logs); diff --git a/runtime/runtime/tests/test_evil_contracts.rs b/runtime/runtime/tests/test_evil_contracts.rs index 457bc4b5ac9..e74f41eb0f9 100644 --- a/runtime/runtime/tests/test_evil_contracts.rs +++ b/runtime/runtime/tests/test_evil_contracts.rs @@ -1,6 +1,7 @@ -use near_primitives::errors::ActionError; +use near_primitives::errors::{ActionError, ActionErrorKind}; use near_primitives::serialize::to_base64; use near_primitives::views::FinalExecutionStatus; +use near_vm_errors::{FunctionExecError, HostError, VMError}; use std::mem::size_of; use testlib::node::{Node, RuntimeNode}; @@ -134,8 +135,13 @@ fn test_evil_abort() { assert_eq!( res.status, FinalExecutionStatus::Failure( - ActionError::FunctionCallError("String encoding is bad UTF-16 sequence.".to_string()) - .into() + ActionError { + index: Some(0), + kind: ActionErrorKind::FunctionCall(VMError::FunctionExecError( + FunctionExecError::HostError(HostError::BadUTF16) + )) + } + .into() ), "{:?}", res diff --git a/scripts/parallel_run_tests.py b/scripts/parallel_run_tests.py index 6879fe47876..695e9c75362 100644 --- a/scripts/parallel_run_tests.py +++ b/scripts/parallel_run_tests.py @@ -17,7 +17,7 @@ def show_test_result(binary, result): if __name__ == "__main__": clean_binary_tests() build_tests() - binaries = test_binaries(exclude=[r'test_regression-.*']) + binaries = test_binaries(exclude=[r'test_regression-.*', r'near_rpc_error_macro-.*']) print(f'========= collected {len(binaries)} test binaries:') print('\n'.join(binaries)) diff --git a/scripts/test_nearlib.sh b/scripts/test_nearlib.sh index 63e87089291..92125db7a17 100755 --- a/scripts/test_nearlib.sh +++ b/scripts/test_nearlib.sh @@ -21,9 +21,10 @@ function get_nearlib_nearshell_release () { function get_nearlib_nearshell_git () { rm -rf nearlib - git clone --single-branch --branch master https://github.com/nearprotocol/nearlib.git nearlib - rm -rf near-shell - git clone --single-branch --branch master https://git@github.com/nearprotocol/near-shell.git near-shell + git clone https://github.com/nearprotocol/nearlib.git nearlib + cd nearlib + git checkout 436ed6339337e11cc4aca79cd43dd5f27feadd39 + cd .. } if [ -z "${NEARLIB_RELEASE}" ]; then @@ -39,10 +40,3 @@ yarn build ../scripts/waitonserver.sh yarn test yarn doc -cd .. - -# Try creating and building new project using NEAR CLI tools -cd near-shell -yarn -#yarn test -cd .. diff --git a/test-utils/testlib/Cargo.toml b/test-utils/testlib/Cargo.toml index 433c36541f1..368af4d09b9 100644 --- a/test-utils/testlib/Cargo.toml +++ b/test-utils/testlib/Cargo.toml @@ -26,6 +26,7 @@ near-crypto = { path = "../../core/crypto" } near-primitives = { path = "../../core/primitives" } near-store = { path = "../../core/store" } node-runtime = { path = "../../runtime/runtime" } +near-vm-errors = { path = "../../runtime/near-vm-errors" } near-chain = { path = "../../chain/chain" } near-client = { path = "../../chain/client" } near-jsonrpc = { path = "../../chain/jsonrpc" } diff --git a/test-utils/testlib/src/node/mod.rs b/test-utils/testlib/src/node/mod.rs index d466afbbe73..419af12c46d 100644 --- a/test-utils/testlib/src/node/mod.rs +++ b/test-utils/testlib/src/node/mod.rs @@ -9,6 +9,7 @@ use near::config::{ }; use near::NearConfig; use near_crypto::{InMemorySigner, Signer}; +use near_jsonrpc::ServerError; use near_primitives::serialize::to_base64; use near_primitives::transaction::SignedTransaction; use near_primitives::types::{AccountId, Balance, NumSeats}; @@ -66,7 +67,7 @@ pub trait Node: Send + Sync { self.user().view_balance(account_id) } - fn add_transaction(&self, transaction: SignedTransaction) -> Result<(), String> { + fn add_transaction(&self, transaction: SignedTransaction) -> Result<(), ServerError> { self.user().add_transaction(transaction) } diff --git a/test-utils/testlib/src/standard_test_cases.rs b/test-utils/testlib/src/standard_test_cases.rs index 62c7564ba13..20f87ff0f1e 100644 --- a/test-utils/testlib/src/standard_test_cases.rs +++ b/test-utils/testlib/src/standard_test_cases.rs @@ -2,13 +2,17 @@ use std::sync::Arc; use near::config::{TESTING_INIT_BALANCE, TESTING_INIT_STAKE}; use near_crypto::{InMemorySigner, KeyType}; +use near_jsonrpc::ServerError; use near_primitives::account::{AccessKey, AccessKeyPermission, FunctionCallPermission}; -use near_primitives::errors::{ActionError, InvalidAccessKeyError, InvalidTxError}; +use near_primitives::errors::{ + ActionError, ActionErrorKind, InvalidAccessKeyError, InvalidTxError, TxExecutionError, +}; use near_primitives::hash::hash; use near_primitives::serialize::to_base64; use near_primitives::types::Balance; use near_primitives::views::FinalExecutionStatus; use near_primitives::views::{AccountView, FinalExecutionOutcomeView}; +use near_vm_errors::{FunctionExecError, HostError, MethodResolveError, VMError}; use crate::fees_utils::FeeHelper; use crate::node::Node; @@ -76,7 +80,15 @@ pub fn test_smart_contract_panic(node: impl Node) { assert_eq!( transaction_result.status, FinalExecutionStatus::Failure( - ActionError::FunctionCallError("Smart contract panicked: WAT?".to_string()).into() + ActionError { + index: Some(0), + kind: ActionErrorKind::FunctionCall(VMError::FunctionExecError( + FunctionExecError::HostError(HostError::GuestPanic { + panic_msg: "WAT?".to_string() + }) + )) + } + .into() ) ); assert_eq!(transaction_result.receipts_outcome.len(), 2); @@ -108,7 +120,13 @@ pub fn test_smart_contract_bad_method_name(node: impl Node) { assert_eq!( transaction_result.status, FinalExecutionStatus::Failure( - ActionError::FunctionCallError("MethodNotFound".to_string()).into() + ActionError { + index: Some(0), + kind: ActionErrorKind::FunctionCall(VMError::FunctionExecError( + FunctionExecError::MethodResolveError(MethodResolveError::MethodNotFound) + )) + } + .into() ) ); assert_eq!(transaction_result.receipts_outcome.len(), 2); @@ -126,7 +144,13 @@ pub fn test_smart_contract_empty_method_name_with_no_tokens(node: impl Node) { assert_eq!( transaction_result.status, FinalExecutionStatus::Failure( - ActionError::FunctionCallError("MethodEmptyName".to_string()).into() + ActionError { + index: Some(0), + kind: ActionErrorKind::FunctionCall(VMError::FunctionExecError( + FunctionExecError::MethodResolveError(MethodResolveError::MethodEmptyName) + )) + } + .into() ) ); assert_eq!(transaction_result.receipts_outcome.len(), 2); @@ -144,7 +168,13 @@ pub fn test_smart_contract_empty_method_name_with_tokens(node: impl Node) { assert_eq!( transaction_result.status, FinalExecutionStatus::Failure( - ActionError::FunctionCallError("MethodEmptyName".to_string()).into() + ActionError { + index: Some(0), + kind: ActionErrorKind::FunctionCall(VMError::FunctionExecError( + FunctionExecError::MethodResolveError(MethodResolveError::MethodEmptyName) + )) + } + .into() ) ); assert_eq!(transaction_result.receipts_outcome.len(), 2); @@ -345,8 +375,11 @@ pub fn test_refund_on_send_money_to_non_existent_account(node: impl Node) { assert_eq!( transaction_result.status, FinalExecutionStatus::Failure( - ActionError::AccountDoesNotExist("Transfer".to_string(), eve_dot_alice_account()) - .into() + ActionError { + index: Some(0), + kind: ActionErrorKind::AccountDoesNotExist { account_id: eve_dot_alice_account() } + } + .into() ) ); assert_eq!(transaction_result.receipts_outcome.len(), 2); @@ -437,7 +470,11 @@ pub fn test_create_account_again(node: impl Node) { assert_eq!( transaction_result.status, FinalExecutionStatus::Failure( - ActionError::AccountAlreadyExists(eve_dot_alice_account()).into() + ActionError { + index: Some(0), + kind: ActionErrorKind::AccountAlreadyExists { account_id: eve_dot_alice_account() } + } + .into() ) ); assert_eq!(transaction_result.receipts_outcome.len(), 2); @@ -477,7 +514,9 @@ pub fn test_create_account_failure_invalid_name(node: impl Node) { .unwrap_err(); assert_eq!( transaction_result, - format!("{}", InvalidTxError::InvalidReceiver(invalid_account_name.to_string())) + ServerError::TxExecutionError(TxExecutionError::InvalidTxError( + InvalidTxError::InvalidReceiverId { receiver_id: invalid_account_name.to_string() } + )) ); } } @@ -496,7 +535,13 @@ pub fn test_create_account_failure_already_exists(node: impl Node) { fee_helper.create_account_transfer_full_key_cost_fail_on_create_account(); assert_eq!( transaction_result.status, - FinalExecutionStatus::Failure(ActionError::AccountAlreadyExists(bob_account()).into()) + FinalExecutionStatus::Failure( + ActionError { + index: Some(0), + kind: ActionErrorKind::AccountAlreadyExists { account_id: bob_account() } + } + .into() + ) ); assert_eq!(transaction_result.receipts_outcome.len(), 2); let new_root = node_user.get_state_root(); @@ -573,7 +618,14 @@ pub fn test_add_existing_key(node: impl Node) { assert_eq!( transaction_result.status, FinalExecutionStatus::Failure( - ActionError::AddKeyAlreadyExists(node.signer().public_key()).into() + ActionError { + index: Some(0), + kind: ActionErrorKind::AddKeyAlreadyExists { + account_id: account_id.clone(), + public_key: node.signer().public_key() + } + } + .into() ) ); assert_eq!(transaction_result.receipts_outcome.len(), 1); @@ -618,7 +670,14 @@ pub fn test_delete_key_not_owned(node: impl Node) { assert_eq!( transaction_result.status, FinalExecutionStatus::Failure( - ActionError::DeleteKeyDoesNotExist(account_id.clone()).into() + ActionError { + index: Some(0), + kind: ActionErrorKind::DeleteKeyDoesNotExist { + account_id: account_id.clone(), + public_key: signer2.public_key.clone() + } + } + .into() ) ); assert_eq!(transaction_result.receipts_outcome.len(), 1); @@ -824,12 +883,11 @@ pub fn test_access_key_smart_contract_reject_method_name(node: impl Node) { .unwrap_err(); assert_eq!( transaction_result, - format!( - "{}", - InvalidTxError::InvalidAccessKey(InvalidAccessKeyError::MethodNameMismatch( - "run_test".to_string() - )) - ) + ServerError::TxExecutionError(TxExecutionError::InvalidTxError( + InvalidTxError::InvalidAccessKeyError(InvalidAccessKeyError::MethodNameMismatch { + method_name: "run_test".to_string() + }) + )) ); } @@ -860,13 +918,12 @@ pub fn test_access_key_smart_contract_reject_contract_id(node: impl Node) { .unwrap_err(); assert_eq!( transaction_result, - format!( - "{}", - InvalidTxError::InvalidAccessKey(InvalidAccessKeyError::ReceiverMismatch( - eve_dot_alice_account(), - bob_account(), - )) - ) + ServerError::TxExecutionError(TxExecutionError::InvalidTxError( + InvalidTxError::InvalidAccessKeyError(InvalidAccessKeyError::ReceiverMismatch { + tx_receiver: eve_dot_alice_account(), + ak_receiver: bob_account() + }) + )) ); } @@ -889,7 +946,9 @@ pub fn test_access_key_reject_non_function_call(node: impl Node) { node_user.delete_key(account_id.clone(), node.signer().public_key()).unwrap_err(); assert_eq!( transaction_result, - format!("{}", InvalidTxError::InvalidAccessKey(InvalidAccessKeyError::ActionError)) + ServerError::TxExecutionError(TxExecutionError::InvalidTxError( + InvalidTxError::InvalidAccessKeyError(InvalidAccessKeyError::RequiresFullAccess) + )) ); } @@ -950,7 +1009,13 @@ pub fn test_unstake_while_not_staked(node: impl Node) { node_user.stake(eve_dot_alice_account(), node.block_signer().public_key(), 0).unwrap(); assert_eq!( transaction_result.status, - FinalExecutionStatus::Failure(ActionError::TriesToUnstake(eve_dot_alice_account()).into()) + FinalExecutionStatus::Failure( + ActionError { + index: Some(0), + kind: ActionErrorKind::TriesToUnstake { account_id: eve_dot_alice_account() } + } + .into() + ) ); assert_eq!(transaction_result.receipts_outcome.len(), 1); } @@ -983,7 +1048,14 @@ fn test_stake_fail_not_enough_rent_with_balance(node: impl Node, initial_balance node_user.stake(new_account_id.clone(), node.block_signer().public_key(), 5).unwrap(); assert_matches!( &transaction_result.status, - FinalExecutionStatus::Failure(e) if e.error_type == "ActionError::RentUnpaid" + FinalExecutionStatus::Failure(e) => match &e { + &TxExecutionError::ActionError(action_err) => + match action_err.kind { + ActionErrorKind::RentUnpaid{..} => {}, + _ => panic!("should be RentUnpaid") + } + _ => panic!("should be Action") + } ); assert_eq!(transaction_result.receipts_outcome.len(), 1); } @@ -1025,7 +1097,13 @@ pub fn test_delete_account_fail(node: impl Node) { let transaction_result = node_user.delete_account(alice_account(), bob_account()).unwrap(); assert_eq!( transaction_result.status, - FinalExecutionStatus::Failure(ActionError::DeleteAccountStaking(bob_account()).into()) + FinalExecutionStatus::Failure( + ActionError { + index: Some(0), + kind: ActionErrorKind::DeleteAccountStaking { account_id: bob_account() } + } + .into() + ) ); assert_eq!(transaction_result.receipts_outcome.len(), 1); assert!(node.user().view_account(&bob_account()).is_ok()); @@ -1042,8 +1120,11 @@ pub fn test_delete_account_no_account(node: impl Node) { assert_eq!( transaction_result.status, FinalExecutionStatus::Failure( - ActionError::AccountDoesNotExist("DeleteAccount".to_string(), eve_dot_alice_account()) - .into() + ActionError { + index: Some(0), + kind: ActionErrorKind::AccountDoesNotExist { account_id: eve_dot_alice_account() } + } + .into() ) ); assert_eq!(transaction_result.receipts_outcome.len(), 1); @@ -1070,7 +1151,11 @@ pub fn test_delete_account_while_staking(node: impl Node) { assert_eq!( transaction_result.status, FinalExecutionStatus::Failure( - ActionError::DeleteAccountStaking(eve_dot_alice_account()).into() + ActionError { + index: Some(0), + kind: ActionErrorKind::DeleteAccountStaking { account_id: eve_dot_alice_account() } + } + .into() ) ); assert_eq!(transaction_result.receipts_outcome.len(), 1); diff --git a/test-utils/testlib/src/user/mod.rs b/test-utils/testlib/src/user/mod.rs index 9deca9f1d60..98b2702f56e 100644 --- a/test-utils/testlib/src/user/mod.rs +++ b/test-utils/testlib/src/user/mod.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use futures::{future::LocalBoxFuture, FutureExt}; use near_crypto::{PublicKey, Signer}; +use near_jsonrpc::ServerError; use near_primitives::account::AccessKey; use near_primitives::hash::CryptoHash; use near_primitives::receipt::Receipt; @@ -31,14 +32,14 @@ pub trait User { fn view_state(&self, account_id: &AccountId, prefix: &[u8]) -> Result; - fn add_transaction(&self, signed_transaction: SignedTransaction) -> Result<(), String>; + fn add_transaction(&self, signed_transaction: SignedTransaction) -> Result<(), ServerError>; fn commit_transaction( &self, signed_transaction: SignedTransaction, - ) -> Result; + ) -> Result; - fn add_receipt(&self, receipt: Receipt) -> Result<(), String>; + fn add_receipt(&self, receipt: Receipt) -> Result<(), ServerError>; fn get_access_key_nonce_for_signer(&self, account_id: &AccountId) -> Result { self.get_access_key(account_id, &self.signer().public_key()) @@ -72,7 +73,7 @@ pub trait User { signer_id: AccountId, receiver_id: AccountId, actions: Vec, - ) -> Result { + ) -> Result { let block_hash = self.get_best_block_hash().unwrap_or(CryptoHash::default()); let signed_transaction = SignedTransaction::from_actions( self.get_access_key_nonce_for_signer(&signer_id).unwrap_or_default() + 1, @@ -90,7 +91,7 @@ pub trait User { signer_id: AccountId, receiver_id: AccountId, amount: Balance, - ) -> Result { + ) -> Result { self.sign_and_commit_actions( signer_id, receiver_id, @@ -102,7 +103,7 @@ pub trait User { &self, signer_id: AccountId, code: Vec, - ) -> Result { + ) -> Result { self.sign_and_commit_actions( signer_id.clone(), signer_id, @@ -118,7 +119,7 @@ pub trait User { args: Vec, gas: Gas, deposit: Balance, - ) -> Result { + ) -> Result { self.sign_and_commit_actions( signer_id, contract_id, @@ -137,7 +138,7 @@ pub trait User { new_account_id: AccountId, public_key: PublicKey, amount: Balance, - ) -> Result { + ) -> Result { self.sign_and_commit_actions( signer_id, new_account_id, @@ -154,7 +155,7 @@ pub trait User { signer_id: AccountId, public_key: PublicKey, access_key: AccessKey, - ) -> Result { + ) -> Result { self.sign_and_commit_actions( signer_id.clone(), signer_id, @@ -166,7 +167,7 @@ pub trait User { &self, signer_id: AccountId, public_key: PublicKey, - ) -> Result { + ) -> Result { self.sign_and_commit_actions( signer_id.clone(), signer_id, @@ -180,7 +181,7 @@ pub trait User { old_public_key: PublicKey, new_public_key: PublicKey, access_key: AccessKey, - ) -> Result { + ) -> Result { self.sign_and_commit_actions( signer_id.clone(), signer_id, @@ -195,7 +196,7 @@ pub trait User { &self, signer_id: AccountId, receiver_id: AccountId, - ) -> Result { + ) -> Result { self.sign_and_commit_actions( signer_id.clone(), receiver_id, @@ -208,7 +209,7 @@ pub trait User { signer_id: AccountId, public_key: PublicKey, stake: Balance, - ) -> Result { + ) -> Result { self.sign_and_commit_actions( signer_id.clone(), signer_id, @@ -222,49 +223,49 @@ pub trait AsyncUser: Send + Sync { fn view_account( &self, account_id: &AccountId, - ) -> LocalBoxFuture<'static, Result>; + ) -> LocalBoxFuture<'static, Result>; fn view_balance( &self, account_id: &AccountId, - ) -> LocalBoxFuture<'static, Result> { + ) -> LocalBoxFuture<'static, Result> { self.view_account(account_id).map(|res| res.map(|acc| acc.amount)).boxed_local() } fn view_state( &self, account_id: &AccountId, - ) -> LocalBoxFuture<'static, Result>; + ) -> LocalBoxFuture<'static, Result>; fn add_transaction( &self, transaction: SignedTransaction, - ) -> LocalBoxFuture<'static, Result<(), String>>; + ) -> LocalBoxFuture<'static, Result<(), ServerError>>; - fn add_receipt(&self, receipt: Receipt) -> LocalBoxFuture<'static, Result<(), String>>; + fn add_receipt(&self, receipt: Receipt) -> LocalBoxFuture<'static, Result<(), ServerError>>; fn get_account_nonce( &self, account_id: &AccountId, - ) -> LocalBoxFuture<'static, Result>; + ) -> LocalBoxFuture<'static, Result>; - fn get_best_height(&self) -> LocalBoxFuture<'static, Result>; + fn get_best_height(&self) -> LocalBoxFuture<'static, Result>; fn get_transaction_result( &self, hash: &CryptoHash, - ) -> LocalBoxFuture<'static, Result>; + ) -> LocalBoxFuture<'static, Result>; fn get_transaction_final_result( &self, hash: &CryptoHash, - ) -> LocalBoxFuture<'static, Result>; + ) -> LocalBoxFuture<'static, Result>; - fn get_state_root(&self) -> LocalBoxFuture<'static, Result>; + fn get_state_root(&self) -> LocalBoxFuture<'static, Result>; fn get_access_key( &self, account_id: &AccountId, public_key: &PublicKey, - ) -> LocalBoxFuture<'static, Result, String>>; + ) -> LocalBoxFuture<'static, Result, ServerError>>; } diff --git a/test-utils/testlib/src/user/rpc_user.rs b/test-utils/testlib/src/user/rpc_user.rs index b5683120548..39055441e99 100644 --- a/test-utils/testlib/src/user/rpc_user.rs +++ b/test-utils/testlib/src/user/rpc_user.rs @@ -10,14 +10,15 @@ use futures::{Future, TryFutureExt}; use near_client::StatusResponse; use near_crypto::{PublicKey, Signer}; use near_jsonrpc::client::{new_client, JsonRpcClient}; +use near_jsonrpc::ServerError; use near_primitives::hash::CryptoHash; use near_primitives::receipt::Receipt; use near_primitives::serialize::{to_base, to_base64}; use near_primitives::transaction::SignedTransaction; use near_primitives::types::{AccountId, BlockHeight, BlockId, MaybeBlockId}; use near_primitives::views::{ - AccessKeyView, AccountView, BlockView, EpochValidatorInfo, ExecutionErrorView, - ExecutionOutcomeView, FinalExecutionOutcomeView, QueryResponse, ViewStateResult, + AccessKeyView, AccountView, BlockView, EpochValidatorInfo, ExecutionOutcomeView, + FinalExecutionOutcomeView, QueryResponse, ViewStateResult, }; use crate::user::User; @@ -66,18 +67,23 @@ impl User for RpcUser { self.query(format!("contract/{}", account_id), prefix)?.try_into() } - fn add_transaction(&self, transaction: SignedTransaction) -> Result<(), String> { + fn add_transaction(&self, transaction: SignedTransaction) -> Result<(), ServerError> { let bytes = transaction.try_to_vec().unwrap(); let _ = self .actix(move |mut client| client.broadcast_tx_async(to_base64(&bytes))) - .map_err(|err| err.to_string())?; + .map_err(|err| { + serde_json::from_value::( + err.data.expect("server error must carry data"), + ) + .expect("deserialize server error must be ok") + })?; Ok(()) } fn commit_transaction( &self, transaction: SignedTransaction, - ) -> Result { + ) -> Result { let bytes = transaction.try_to_vec().unwrap(); let result = self.actix(move |mut client| client.broadcast_tx_commit(to_base64(&bytes))); // Wait for one more block, to make sure all nodes actually apply the state transition. @@ -87,16 +93,11 @@ impl User for RpcUser { } match result { Ok(outcome) => Ok(outcome), - Err(err) => { - match serde_json::from_value::(err.clone().data.unwrap()) { - Ok(error_view) => Err(error_view.error_message), - Err(_) => Err(serde_json::to_string(&err).unwrap()), - } - } + Err(err) => Err(serde_json::from_value::(err.data.unwrap()).unwrap()), } } - fn add_receipt(&self, _receipt: Receipt) -> Result<(), String> { + fn add_receipt(&self, _receipt: Receipt) -> Result<(), ServerError> { // TDDO: figure out if rpc will support this unimplemented!() } diff --git a/test-utils/testlib/src/user/runtime_user.rs b/test-utils/testlib/src/user/runtime_user.rs index c5877d13aad..d33714e8bc7 100644 --- a/test-utils/testlib/src/user/runtime_user.rs +++ b/test-utils/testlib/src/user/runtime_user.rs @@ -3,7 +3,8 @@ use std::collections::{HashMap, HashSet}; use std::sync::{Arc, RwLock}; use near_crypto::{PublicKey, Signer}; -use near_primitives::errors::RuntimeError; +use near_jsonrpc::ServerError; +use near_primitives::errors::{RuntimeError, TxExecutionError}; use near_primitives::hash::CryptoHash; use near_primitives::receipt::Receipt; use near_primitives::transaction::SignedTransaction; @@ -66,7 +67,7 @@ impl RuntimeUser { apply_state: ApplyState, prev_receipts: Vec, transactions: Vec, - ) -> Result<(), String> { + ) -> Result<(), ServerError> { let mut receipts = prev_receipts; for transaction in transactions.iter() { self.transactions.borrow_mut().insert(transaction.clone()); @@ -86,8 +87,10 @@ impl RuntimeUser { &HashSet::new(), ) .map_err(|e| match e { - RuntimeError::InvalidTxError(e) => format!("{}", e), - RuntimeError::BalanceMismatch(e) => panic!("{}", e), + RuntimeError::InvalidTxError(e) => { + ServerError::TxExecutionError(TxExecutionError::InvalidTxError(e)) + } + RuntimeError::BalanceMismatchError(e) => panic!("{}", e), RuntimeError::StorageError(e) => panic!("Storage error {:?}", e), RuntimeError::UnexpectedIntegerOverflow => { panic!("UnexpectedIntegerOverflow error") @@ -197,7 +200,7 @@ impl User for RuntimeUser { .map_err(|err| err.to_string()) } - fn add_transaction(&self, transaction: SignedTransaction) -> Result<(), String> { + fn add_transaction(&self, transaction: SignedTransaction) -> Result<(), ServerError> { self.apply_all(self.apply_state(), vec![], vec![transaction])?; Ok(()) } @@ -205,12 +208,12 @@ impl User for RuntimeUser { fn commit_transaction( &self, transaction: SignedTransaction, - ) -> Result { + ) -> Result { self.apply_all(self.apply_state(), vec![], vec![transaction.clone()])?; Ok(self.get_transaction_final_result(&transaction.get_hash())) } - fn add_receipt(&self, receipt: Receipt) -> Result<(), String> { + fn add_receipt(&self, receipt: Receipt) -> Result<(), ServerError> { self.apply_all(self.apply_state(), vec![receipt], vec![])?; Ok(()) } diff --git a/tests/test_errors.rs b/tests/test_errors.rs index 8bacd81c1ef..9c39ce2e0ab 100644 --- a/tests/test_errors.rs +++ b/tests/test_errors.rs @@ -47,13 +47,11 @@ fn test_check_tx_error_log() { let tx_result = node.user().commit_transaction(tx).unwrap_err(); assert_eq!( tx_result, - format!( - "{}", - InvalidTxError::InvalidAccessKey(InvalidAccessKeyError::AccessKeyNotFound( - "bob.near".to_string(), - signer.public_key.clone() - )) - ), + InvalidTxError::InvalidAccessKeyError(InvalidAccessKeyError::AccessKeyNotFound { + account_id: "bob.near".to_string(), + public_key: signer.public_key.clone() + }) + .into() ); } @@ -86,13 +84,11 @@ fn test_deliver_tx_error_log() { let tx_result = node.user().commit_transaction(tx).unwrap_err(); assert_eq!( tx_result, - format!( - "{}", - InvalidTxError::NotEnoughBalance( - "alice.near".to_string(), - TESTING_INIT_BALANCE - TESTING_INIT_STAKE, - TESTING_INIT_BALANCE + 1 + cost - ) - ), + InvalidTxError::NotEnoughBalance { + signer_id: "alice.near".to_string(), + balance: TESTING_INIT_BALANCE - TESTING_INIT_STAKE, + cost: TESTING_INIT_BALANCE + 1 + cost + } + .into() ); } diff --git a/tools/rpctypegen/core/Cargo.toml b/tools/rpctypegen/core/Cargo.toml new file mode 100644 index 00000000000..9ce6952bee1 --- /dev/null +++ b/tools/rpctypegen/core/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "near-rpc-error-core" +version = "0.1.0" +authors = ["Near Inc "] +edition = "2018" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = {version = "1.0", features = ["preserve_order"]} +syn = { version = "1.0", features = ["full", "extra-traits"]} +quote = "1.0" +proc-macro2 = "1.0" + +[features] +test = [] +dump_errors_schema = [] diff --git a/tools/rpctypegen/core/src/lib.rs b/tools/rpctypegen/core/src/lib.rs new file mode 100644 index 00000000000..b2f64b9b1e0 --- /dev/null +++ b/tools/rpctypegen/core/src/lib.rs @@ -0,0 +1,303 @@ +extern crate proc_macro; +extern crate proc_macro2; + +use std::collections::BTreeMap; +use syn::{Data, DataEnum, DataStruct, DeriveInput, Fields, FieldsNamed, FieldsUnnamed}; + +use serde::{Deserialize, Serialize}; + +#[derive(Default, Debug, Deserialize, Serialize)] +pub struct ErrorType { + /// A type name of the error + pub name: String, + /// Names of subtypes of the error + pub subtypes: Vec, + /// An error input name and a type + pub props: BTreeMap, +} + +fn parse_rpc_error_variant(input: &DeriveInput) -> String { + let type_name = input.ident.to_string(); + let type_kind: Vec<&str> = type_name.split("Kind").collect(); + type_kind[0].to_string() +} + +fn error_type_name<'a>( + schema: &'a mut BTreeMap, + name: String, +) -> &'a mut ErrorType { + let error_type = ErrorType { name: name.clone(), ..Default::default() }; + schema.entry(name.clone()).or_insert(error_type) +} + +pub fn parse_error_type(schema: &mut BTreeMap, input: &DeriveInput) { + let name = parse_rpc_error_variant(input); + match &input.data { + Data::Enum(DataEnum { ref variants, .. }) => { + // TODO: check for uniqueness + let error_type = error_type_name(schema, name); + let mut direct_error_types = vec![]; + for variant in variants { + error_type.subtypes.push(variant.ident.to_string()); + match &variant.fields { + Fields::Unnamed(FieldsUnnamed { ref unnamed, .. }) => { + // Subtype + if unnamed.iter().count() > 1 { + panic!( + "Error types doesn't support tuple variants with multiple fields" + ); + } + } + Fields::Named(FieldsNamed { ref named, .. }) => { + // If variant is Enum with a named fields - create a new type for each variant with named props + let mut error_type = ErrorType::default(); + error_type.name = variant.ident.to_string(); + for field in named { + error_type.props.insert( + field + .ident + .as_ref() + .expect("named fields must have ident") + .to_string(), + "".to_owned(), + ); + } + direct_error_types.push(error_type); + } + Fields::Unit => { + direct_error_types.push(ErrorType { + name: variant.ident.to_string(), + ..Default::default() + }); + } + } + } + for e in direct_error_types { + let mut error_type = error_type_name(schema, e.name.clone()); + error_type.name = e.name; + error_type.props = e.props; + } + } + Data::Struct(DataStruct { ref fields, .. }) => { + let error_type = error_type_name(schema, name); + match fields { + Fields::Named(FieldsNamed { ref named, .. }) => { + for field in named { + let field_name = + field.ident.as_ref().expect("named fields must have ident").to_string(); + if field_name == "kind" { + continue; + } + error_type.props.insert(field_name, "".to_owned()); // TODO: add prop type + } + } + _ => { + panic!("RpcError supports structs with the named fields only"); + } + } + } + Data::Union(_) => { + panic!("Unions are not supported"); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use quote::quote; + #[test] + fn should_merge_kind() { + let mut schema = BTreeMap::default(); + let error_type = syn::parse2(quote! { + pub struct ActionError { + pub index: Option, + pub kind: ActionErrorKind, + } + }) + .unwrap(); + parse_error_type(&mut schema, &error_type); + let expected: BTreeMap = serde_json::from_str( + r#" + { + "ActionError": { + "name": "ActionError", + "subtypes": [], + "props": { + "index": "" + } + } + } + "#, + ) + .unwrap(); + assert_eq!( + serde_json::to_string(&expected).unwrap(), + serde_json::to_string(&schema).unwrap() + ); + let error_type_kind: DeriveInput = syn::parse2(quote! { + pub enum ActionErrorKind { + AccountAlreadyExists { account_id: String }, + } + }) + .unwrap(); + let expected: BTreeMap = serde_json::from_str( + r#" + { + "ActionError": { + "name": "ActionError", + "subtypes": ["AccountAlreadyExists"], + "props": { + "index": "" + } + }, + "AccountAlreadyExists": { + "name": "AccountAlreadyExists", + "subtypes": [], + "props": { + "account_id": "" + } + } + } + "#, + ) + .unwrap(); + parse_error_type(&mut schema, &error_type_kind); + assert_eq!( + serde_json::to_string(&expected).unwrap(), + serde_json::to_string(&schema).unwrap() + ); + } + + #[test] + fn complex() { + let mut schema = BTreeMap::default(); + parse_error_type( + &mut schema, + &syn::parse2(quote! { + pub enum TxExecutionError { + ActionError(ActionError), + InvalidTxError(InvalidTxError), + } + }) + .unwrap(), + ); + parse_error_type( + &mut schema, + &syn::parse2(quote! { + pub enum InvalidTxError { + InvalidAccessKeyError(InvalidAccessKeyError), + InvalidSignerId { signer_id: AccountId }, + } + }) + .unwrap(), + ); + parse_error_type( + &mut schema, + &syn::parse2(quote! { + pub enum InvalidAccessKeyError { + /// The access key identified by the `public_key` doesn't exist for the account + AccessKeyNotFound { account_id: AccountId, public_key: PublicKey }, + } + }) + .unwrap(), + ); + parse_error_type( + &mut schema, + &syn::parse2(quote! { + pub struct ActionError { + pub index: Option, + pub kind: ActionErrorKind, + } + }) + .unwrap(), + ); + parse_error_type( + &mut schema, + &syn::parse2(quote! { + pub enum ActionErrorKind { + AccountAlreadyExists { account_id: String }, + } + }) + .unwrap(), + ); + let expected: BTreeMap = serde_json::from_str( + r#" + { + "AccessKeyNotFound": { + "name": "AccessKeyNotFound", + "subtypes": [], + "props": { + "account_id": "", + "public_key": "" + } + }, + "AccountAlreadyExists": { + "name": "AccountAlreadyExists", + "subtypes": [], + "props": { + "account_id": "" + } + }, + "ActionError": { + "name": "ActionError", + "subtypes": [ + "AccountAlreadyExists" + ], + "props": { + "index": "" + } + }, + "InvalidAccessKeyError": { + "name": "InvalidAccessKeyError", + "subtypes": [ + "AccessKeyNotFound" + ], + "props": {} + }, + "InvalidSignerId": { + "name": "InvalidSignerId", + "subtypes": [], + "props": { + "signer_id": "" + } + }, + "InvalidTxError": { + "name": "InvalidTxError", + "subtypes": [ + "InvalidAccessKeyError", + "InvalidSignerId" + ], + "props": {} + }, + "TxExecutionError": { + "name": "TxExecutionError", + "subtypes": [ + "ActionError", + "InvalidTxError" + ], + "props": {} + } + }"#, + ) + .unwrap(); + assert_eq!( + serde_json::to_string(&expected).unwrap(), + serde_json::to_string(&schema).unwrap() + ); + } + #[test] + #[should_panic] + fn should_not_accept_tuples() { + let mut schema = BTreeMap::default(); + parse_error_type( + &mut schema, + &syn::parse2(quote! { + pub enum ErrorWithATupleVariant { + Var(One, Two) + } + }) + .unwrap(), + ); + } +} diff --git a/tools/rpctypegen/macro/Cargo.toml b/tools/rpctypegen/macro/Cargo.toml new file mode 100644 index 00000000000..0d382a8b203 --- /dev/null +++ b/tools/rpctypegen/macro/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "near-rpc-error-macro" +version = "0.1.0" +authors = ["Near Inc "] +edition = "2018" + +[lib] +proc-macro = true + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = {version = "1.0", features = ["preserve_order"]} +syn = { version = "1.0", features = ["full", "extra-traits"]} +quote = "1.0" +proc-macro2 = "1.0" +near-rpc-error-core = { path = "../core/." } + +[features] +test = [] +dump_errors_schema = ["near-rpc-error-core/dump_errors_schema"] diff --git a/tools/rpctypegen/macro/src/lib.rs b/tools/rpctypegen/macro/src/lib.rs new file mode 100644 index 00000000000..e44f9f9782d --- /dev/null +++ b/tools/rpctypegen/macro/src/lib.rs @@ -0,0 +1,64 @@ +extern crate proc_macro; +extern crate proc_macro2; + +use near_rpc_error_core::{parse_error_type, ErrorType}; +use proc_macro::TokenStream; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "dump_errors_schema")] +use serde_json::Value; +use std::cell::RefCell; +use std::collections::BTreeMap; +use syn::{parse_macro_input, DeriveInput}; + +thread_local!(static SCHEMA: RefCell = RefCell::new(Schema::default())); + +#[derive(Default, Debug, Deserialize, Serialize)] +struct Schema { + pub schema: BTreeMap, +} + +#[cfg(feature = "dump_errors_schema")] +fn merge(a: &mut Value, b: &Value) { + match (a, b) { + (&mut Value::Object(ref mut a), &Value::Object(ref b)) => { + for (k, v) in b { + merge(a.entry(k.clone()).or_insert(Value::Null), v); + } + } + (a, b) => { + *a = b.clone(); + } + } +} + +#[cfg(feature = "dump_errors_schema")] +impl Drop for Schema { + fn drop(&mut self) { + // std::env::var("CARGO_TARGET_DIR") doesn't exists + let filename = "./target/rpc_errors_schema.json"; + let schema_json = serde_json::to_value(self).expect("Schema serialize failed"); + let new_schema_json = if let Ok(data) = std::fs::read(filename) { + // merge to the existing file + let mut existing_schema = serde_json::from_slice::(&data) + .expect("cannot deserialize target/existing_schema.json"); + merge(&mut existing_schema, &schema_json); + existing_schema + } else { + schema_json + }; + let new_schema_json_string = serde_json::to_string_pretty(&new_schema_json) + .expect("error schema serialization failed"); + std::fs::write(filename, new_schema_json_string) + .expect("Unable to save the errors schema file"); + } +} + +#[proc_macro_derive(RpcError)] +pub fn rpc_error(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + SCHEMA.with(|s| { + parse_error_type(&mut s.borrow_mut().schema, &input); + }); + TokenStream::new() +}