diff --git a/Cargo.lock b/Cargo.lock index bc5ecedd133..df43f08214e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3236,12 +3236,13 @@ dependencies = [ [[package]] name = "near-jsonrpc" -version = "0.2.0" +version = "0.2.1" dependencies = [ "actix", "actix-cors", "actix-web", "borsh", + "easy-ext", "futures", "lazy_static", "near-actix-test-utils", @@ -3256,7 +3257,6 @@ dependencies = [ "near-performance-metrics", "near-primitives", "near-rpc-error-macro", - "near-runtime-utils", "prometheus", "serde", "serde_json", @@ -3290,6 +3290,8 @@ dependencies = [ "near-metrics", "near-primitives", "near-primitives-core", + "near-rpc-error-macro", + "near-runtime-utils", "serde", "serde_json", "thiserror", diff --git a/chain/client-primitives/src/types.rs b/chain/client-primitives/src/types.rs index 0c64048f5d6..c1e8b277cd7 100644 --- a/chain/client-primitives/src/types.rs +++ b/chain/client-primitives/src/types.rs @@ -418,7 +418,7 @@ pub enum TxStatusError { ChainError(near_chain_primitives::Error), MissingTransaction(CryptoHash), InvalidTx(InvalidTxError), - InternalError, + InternalError(String), TimeoutError, } @@ -429,7 +429,9 @@ impl From for String { TxStatusError::MissingTransaction(tx_hash) => { format!("Transaction {} doesn't exist", tx_hash) } - TxStatusError::InternalError => format!("Internal error"), + TxStatusError::InternalError(debug_message) => { + format!("Internal error: {}", debug_message) + } TxStatusError::TimeoutError => format!("Timeout error"), TxStatusError::InvalidTx(e) => format!("Invalid transaction: {}", e), } diff --git a/chain/jsonrpc-primitives/Cargo.toml b/chain/jsonrpc-primitives/Cargo.toml index 4608efdd4db..b132191bf0f 100644 --- a/chain/jsonrpc-primitives/Cargo.toml +++ b/chain/jsonrpc-primitives/Cargo.toml @@ -21,3 +21,5 @@ near-crypto = { path = "../../core/crypto" } near-metrics = { path = "../../core/metrics" } near-primitives = { path = "../../core/primitives" } near-primitives-core = { path = "../../core/primitives-core" } +near-rpc-error-macro = { path = "../../tools/rpctypegen/macro" } +near-runtime-utils = { path = "../../runtime/near-runtime-utils" } diff --git a/chain/jsonrpc-primitives/src/errors.rs b/chain/jsonrpc-primitives/src/errors.rs index cd77af8c45e..571e5fb3294 100644 --- a/chain/jsonrpc-primitives/src/errors.rs +++ b/chain/jsonrpc-primitives/src/errors.rs @@ -1,6 +1,10 @@ +use std::fmt::Display; + use serde::{Deserialize, Serialize}; use serde_json::{to_value, Value}; +use near_primitives::errors::{InvalidTxError, TxExecutionError}; + #[derive(Serialize)] pub struct RpcParseError(pub String); @@ -16,6 +20,15 @@ pub struct RpcError { pub data: Option, } +/// A general Server Error +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, near_rpc_error_macro::RpcError)] +pub enum ServerError { + TxExecutionError(TxExecutionError), + Timeout, + Closed, + InternalError, +} + impl RpcError { /// A generic constructor. /// @@ -75,3 +88,35 @@ impl From for RpcError { Self::invalid_params(parse_error.0) } } + +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"), + ServerError::InternalError => write!(f, "ServerError: Internal Error"), + } + } +} + +impl From for ServerError { + fn from(e: InvalidTxError) -> ServerError { + ServerError::TxExecutionError(TxExecutionError::InvalidTxError(e)) + } +} + +impl From for ServerError { + fn from(e: actix::MailboxError) -> Self { + match e { + actix::MailboxError::Closed => ServerError::Closed, + actix::MailboxError::Timeout => ServerError::Timeout, + } + } +} + +impl From for RpcError { + fn from(e: ServerError) -> RpcError { + RpcError::server_error(Some(e)) + } +} diff --git a/chain/jsonrpc-primitives/src/rpc.rs b/chain/jsonrpc-primitives/src/rpc.rs index 5a03788e1a3..3ddd282e3d9 100644 --- a/chain/jsonrpc-primitives/src/rpc.rs +++ b/chain/jsonrpc-primitives/src/rpc.rs @@ -6,8 +6,7 @@ use serde::{Deserialize, Serialize}; use near_primitives::hash::CryptoHash; use near_primitives::merkle::MerklePath; -use near_primitives::transaction::SignedTransaction; -use near_primitives::types::{AccountId, BlockReference, MaybeBlockId, TransactionOrReceiptId}; +use near_primitives::types::{BlockReference, MaybeBlockId, TransactionOrReceiptId}; use near_primitives::views::{ ExecutionOutcomeWithIdView, LightClientBlockLiteView, QueryRequest, StateChangeWithCauseView, StateChangesKindsView, StateChangesRequestView, @@ -47,12 +46,6 @@ pub struct RpcStateChangesInBlockResponse { pub changes: StateChangesKindsView, } -#[derive(Serialize, Deserialize)] -pub struct RpcBroadcastTxSyncResponse { - pub transaction_hash: String, - pub is_routed: bool, -} - #[derive(Serialize, Deserialize)] pub struct RpcLightClientExecutionProofRequest { #[serde(flatten)] @@ -68,12 +61,6 @@ pub struct RpcLightClientExecutionProofResponse { pub block_proof: MerklePath, } -#[derive(Clone, Debug)] -pub enum TransactionInfo { - Transaction(SignedTransaction), - TransactionId { hash: CryptoHash, account_id: AccountId }, -} - #[derive(Serialize, Deserialize)] pub struct RpcValidatorsOrderedRequest { pub block_id: MaybeBlockId, diff --git a/chain/jsonrpc-primitives/src/types/mod.rs b/chain/jsonrpc-primitives/src/types/mod.rs index f0aa7db4a45..c9dd15e7320 100644 --- a/chain/jsonrpc-primitives/src/types/mod.rs +++ b/chain/jsonrpc-primitives/src/types/mod.rs @@ -4,4 +4,5 @@ pub mod config; pub mod gas_price; pub mod query; pub mod receipts; +pub mod transactions; pub mod validator; diff --git a/chain/jsonrpc-primitives/src/types/transactions.rs b/chain/jsonrpc-primitives/src/types/transactions.rs new file mode 100644 index 00000000000..c728987ba0c --- /dev/null +++ b/chain/jsonrpc-primitives/src/types/transactions.rs @@ -0,0 +1,125 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Debug, Clone)] +pub struct RpcBroadcastTransactionRequest { + pub signed_transaction: near_primitives::transaction::SignedTransaction, +} + +#[derive(Debug)] +pub struct RpcTransactionStatusCommonRequest { + pub transaction_info: TransactionInfo, +} + +#[derive(Clone, Debug)] +pub enum TransactionInfo { + Transaction(near_primitives::transaction::SignedTransaction), + TransactionId { + hash: near_primitives::hash::CryptoHash, + account_id: near_primitives::types::AccountId, + }, +} + +#[derive(thiserror::Error, Debug)] +pub enum RpcTransactionError { + #[error("An error happened during transaction execution: {context:?}")] + InvalidTransaction { context: near_primitives::errors::InvalidTxError }, + #[error("Node doesn't track this shard. Cannot determine whether the transaction is valid")] + DoesNotTrackShard, + #[error("Transaction with hash {transaction_hash} was routed")] + RequestRouted { transaction_hash: String }, + #[error("Transaction {requested_transaction_hash} doesn't exist")] + UnknownTransaction { requested_transaction_hash: near_primitives::hash::CryptoHash }, + #[error("The node reached its limits. Try again later. More details: {debug_info}")] + InternalError { debug_info: String }, + #[error("Timeout")] + TimeoutError, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct RpcTransactionResponse { + #[serde(flatten)] + pub final_execution_outcome: near_primitives::views::FinalExecutionOutcomeViewEnum, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct RpcBroadcastTxSyncResponse { + pub transaction_hash: String, +} + +impl RpcBroadcastTransactionRequest { + pub fn parse(value: Option) -> Result { + let signed_transaction = crate::utils::parse_signed_transaction(value)?; + Ok(Self { signed_transaction }) + } +} + +impl RpcTransactionStatusCommonRequest { + pub fn parse(value: Option) -> Result { + if let Ok((hash, account_id)) = + crate::utils::parse_params::<(near_primitives::hash::CryptoHash, String)>(value.clone()) + { + if !near_runtime_utils::is_valid_account_id(&account_id) { + return Err(crate::errors::RpcParseError(format!( + "Invalid account id: {}", + account_id + ))); + } + let transaction_info = TransactionInfo::TransactionId { hash, account_id }; + Ok(Self { transaction_info }) + } else { + let signed_transaction = crate::utils::parse_signed_transaction(value)?; + let transaction_info = TransactionInfo::Transaction(signed_transaction); + Ok(Self { transaction_info }) + } + } +} + +impl From for RpcTransactionError { + fn from(error: near_client_primitives::types::TxStatusError) -> Self { + match error { + near_client_primitives::types::TxStatusError::ChainError(err) => { + Self::InternalError { debug_info: format!("{:?}", err) } + } + near_client_primitives::types::TxStatusError::MissingTransaction( + requested_transaction_hash, + ) => Self::UnknownTransaction { requested_transaction_hash }, + near_client_primitives::types::TxStatusError::InvalidTx(context) => { + Self::InvalidTransaction { context } + } + near_client_primitives::types::TxStatusError::InternalError(debug_info) => { + Self::InternalError { debug_info } + } + near_client_primitives::types::TxStatusError::TimeoutError => Self::TimeoutError, + } + } +} + +impl From for RpcTransactionResponse { + fn from( + final_execution_outcome: near_primitives::views::FinalExecutionOutcomeViewEnum, + ) -> Self { + Self { final_execution_outcome } + } +} + +impl From for crate::errors::RpcError { + fn from(error: RpcTransactionError) -> Self { + let error_data = match error { + RpcTransactionError::InvalidTransaction { ref context } => Some( + serde_json::to_value(crate::errors::ServerError::TxExecutionError( + near_primitives::errors::TxExecutionError::InvalidTxError(context.clone()), + )) + .unwrap_or(Value::String(error.to_string())), + ), + _ => Some(Value::String(error.to_string())), + }; + Self::new(-32_000, "Server error".to_string(), error_data) + } +} + +impl From for RpcTransactionError { + fn from(error: actix::MailboxError) -> Self { + Self::InternalError { debug_info: error.to_string() } + } +} diff --git a/chain/jsonrpc-primitives/src/utils.rs b/chain/jsonrpc-primitives/src/utils.rs index 0bb4a2cfb92..0bad7ec2451 100644 --- a/chain/jsonrpc-primitives/src/utils.rs +++ b/chain/jsonrpc-primitives/src/utils.rs @@ -1,6 +1,8 @@ use serde::de::DeserializeOwned; use serde_json::Value; +use near_primitives::borsh::BorshDeserialize; + pub(crate) fn parse_params( value: Option, ) -> Result { @@ -11,3 +13,18 @@ pub(crate) fn parse_params( Err(crate::errors::RpcParseError("Require at least one parameter".to_owned())) } } + +fn from_base64_or_parse_err(encoded: String) -> Result, crate::errors::RpcParseError> { + near_primitives_core::serialize::from_base64(&encoded) + .map_err(|err| crate::errors::RpcParseError(err.to_string())) +} + +pub(crate) fn parse_signed_transaction( + value: Option, +) -> Result { + let (encoded,) = crate::utils::parse_params::<(String,)>(value.clone())?; + let bytes = crate::utils::from_base64_or_parse_err(encoded)?; + Ok(near_primitives::transaction::SignedTransaction::try_from_slice(&bytes).map_err(|err| { + crate::errors::RpcParseError(format!("Failed to decode transaction: {}", err)) + })?) +} diff --git a/chain/jsonrpc/CHANGELOG.md b/chain/jsonrpc/CHANGELOG.md index ad756c212f0..e7c331dee7e 100644 --- a/chain/jsonrpc/CHANGELOG.md +++ b/chain/jsonrpc/CHANGELOG.md @@ -1,6 +1,20 @@ # Changelog -## Unreleased +## 0.2.1 + +* Refactored methods: + * `broadcast_tx_async` + * `broadcast_tx_commit` + * `EXPERIMENTAL_broadcast_tx_sync` + * `EXPERIMENTAL_check_tx` + * `EXPERIMENTAL_tx_status` + * `tx` + +## Breaking changes + +Response from `EXPERIMENTAL_broadcast_tx_sync` and `EXPERIMENTAL_check_tx` doesn't return `is_routed` +field anymore. In case of a transaction getting routed an error will be returned. Also, `EXPERIMENTAL_check_tx` +returns response with transaction hash instead of empty body. * Added `EXPERIMENTAL_tx_status` endpoint exposing receipts in addition to all the rest data available in `tx` endpoint diff --git a/chain/jsonrpc/Cargo.toml b/chain/jsonrpc/Cargo.toml index b2022e656ef..f02f3899a15 100644 --- a/chain/jsonrpc/Cargo.toml +++ b/chain/jsonrpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "near-jsonrpc" -version = "0.2.0" +version = "0.2.1" authors = ["Near Inc "] edition = "2018" @@ -8,6 +8,7 @@ edition = "2018" actix = "0.11.0-beta.2" actix-web = "4.0.0-beta.3" actix-cors = { git = "https://github.com/near/actix-extras.git", branch="actix-web-4-beta.3" } +easy-ext = "0.2" tokio = { version = "1.1", features = ["full"] } futures = "0.3" lazy_static = "1.4" @@ -27,7 +28,6 @@ near-network = { path = "../network" } near-jsonrpc-client = { path = "client" } near-jsonrpc-primitives = { path = "../jsonrpc-primitives" } near-rpc-error-macro = { path = "../../tools/rpctypegen/macro" } -near-runtime-utils = { path = "../../runtime/near-runtime-utils" } near-performance-metrics = { path = "../../utils/near-performance-metrics" } [dev-dependencies] diff --git a/chain/jsonrpc/src/lib.rs b/chain/jsonrpc/src/lib.rs index 08fb7a65740..1e17174ce28 100644 --- a/chain/jsonrpc/src/lib.rs +++ b/chain/jsonrpc/src/lib.rs @@ -1,13 +1,11 @@ -use std::fmt::Display; use std::string::FromUtf8Error; use std::time::Duration; use actix::{Addr, MailboxError}; use actix_cors::Cors; use actix_web::{http, middleware, web, App, Error as HttpError, HttpResponse, HttpServer}; -use borsh::BorshDeserialize; use futures::Future; -use futures::{FutureExt, TryFutureExt}; +use futures::FutureExt; use prometheus; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; @@ -26,23 +24,20 @@ pub use near_jsonrpc_client as client; use near_jsonrpc_primitives::errors::RpcError; use near_jsonrpc_primitives::message::{Message, Request}; use near_jsonrpc_primitives::rpc::{ - RpcBroadcastTxSyncResponse, RpcLightClientExecutionProofRequest, - RpcLightClientExecutionProofResponse, RpcStateChangesInBlockRequest, - RpcStateChangesInBlockResponse, RpcStateChangesRequest, RpcStateChangesResponse, - RpcValidatorsOrderedRequest, TransactionInfo, + RpcLightClientExecutionProofRequest, RpcLightClientExecutionProofResponse, + RpcStateChangesInBlockRequest, RpcStateChangesInBlockResponse, RpcStateChangesRequest, + RpcStateChangesResponse, RpcValidatorsOrderedRequest, }; use near_jsonrpc_primitives::types::config::RpcProtocolConfigResponse; use near_metrics::{Encoder, TextEncoder}; #[cfg(feature = "adversarial")] use near_network::types::{NetworkAdversarialMessage, NetworkViewClientMessages}; use near_network::{NetworkClientMessages, NetworkClientResponses}; -use near_primitives::errors::{InvalidTxError, TxExecutionError}; use near_primitives::hash::CryptoHash; -use near_primitives::serialize::{from_base64, BaseEncode}; +use near_primitives::serialize::BaseEncode; use near_primitives::transaction::SignedTransaction; use near_primitives::types::AccountId; -use near_primitives::views::{FinalExecutionOutcomeView, FinalExecutionOutcomeViewEnum}; -use near_runtime_utils::is_valid_account_id; +use near_primitives::views::FinalExecutionOutcomeViewEnum; mod metrics; @@ -99,10 +94,6 @@ impl RpcConfig { } } -fn from_base64_or_parse_err(encoded: String) -> Result, RpcError> { - from_base64(&encoded).map_err(|err| RpcError::parse_error(err.to_string())) -} - fn parse_params(value: Option) -> Result { if let Some(value) = value { serde_json::from_value(value) @@ -123,58 +114,20 @@ fn jsonify( .map_err(|err| RpcError::server_error(Some(err))) } -fn parse_tx(params: Option) -> Result { - let (encoded,) = parse_params::<(String,)>(params)?; - let bytes = from_base64_or_parse_err(encoded)?; - SignedTransaction::try_from_slice(&bytes) - .map_err(|e| RpcError::invalid_params(format!("Failed to decode transaction: {}", e))) -} - -/// A general Server Error -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, near_rpc_error_macro::RpcError)] -pub enum ServerError { - TxExecutionError(TxExecutionError), - Timeout, - Closed, - InternalError, -} - -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"), - ServerError::InternalError => write!(f, "ServerError: Internal Error"), - } - } -} - -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, +#[easy_ext::ext(FromNetworkClientResponses)] +impl near_jsonrpc_primitives::types::transactions::RpcTransactionError { + pub fn from_network_client_responses(responses: NetworkClientResponses) -> Self { + match responses { + NetworkClientResponses::InvalidTx(context) => Self::InvalidTransaction { context }, + NetworkClientResponses::NoResponse => Self::TimeoutError, + NetworkClientResponses::DoesNotTrackShard | NetworkClientResponses::RequestRouted => { + Self::DoesNotTrackShard + } + internal_error => Self::InternalError { debug_info: format!("{:?}", internal_error) }, } } } -impl From for RpcError { - fn from(e: ServerError) -> RpcError { - RpcError::server_error(Some(e)) - } -} - -fn timeout_err() -> RpcError { - RpcError::server_error(Some(ServerError::Timeout)) -} - /// This function processes response from query method to introduce /// backward compatible response in case of specific errors fn process_query_response( @@ -289,18 +242,50 @@ impl JsonRpcHandler { let block = self.block(rpc_block_request).await?; serde_json::to_value(block).map_err(|err| RpcError::parse_error(err.to_string())) } - "broadcast_tx_async" => self.send_tx_async(request.params).await, - "broadcast_tx_commit" => self.send_tx_commit(request.params).await, + "broadcast_tx_async" => { + let rpc_transaction_request = + near_jsonrpc_primitives::types::transactions::RpcBroadcastTransactionRequest::parse( + request.params, + )?; + let transaction_hash = self.send_tx_async(rpc_transaction_request).await; + serde_json::to_value((&transaction_hash).to_base()) + .map_err(|err| RpcError::parse_error(err.to_string())) + } + "broadcast_tx_commit" => { + let rpc_transaction_request = + near_jsonrpc_primitives::types::transactions::RpcBroadcastTransactionRequest::parse( + request.params, + )?; + let send_tx_response = self.send_tx_commit(rpc_transaction_request).await?; + serde_json::to_value(send_tx_response) + .map_err(|err| RpcError::parse_error(err.to_string())) + } "chunk" => { let rpc_chunk_request = near_jsonrpc_primitives::types::chunks::RpcChunkRequest::parse(request.params)?; let chunk = self.chunk(rpc_chunk_request).await?; serde_json::to_value(chunk).map_err(|err| RpcError::parse_error(err.to_string())) } - "EXPERIMENTAL_broadcast_tx_sync" => self.send_tx_sync(request.params).await, + "EXPERIMENTAL_broadcast_tx_sync" => { + let rpc_transaction_request = + near_jsonrpc_primitives::types::transactions::RpcBroadcastTransactionRequest::parse( + request.params, + )?; + let broadcast_tx_sync_response = self.send_tx_sync(rpc_transaction_request).await?; + serde_json::to_value(broadcast_tx_sync_response) + .map_err(|err| RpcError::parse_error(err.to_string())) + } "EXPERIMENTAL_changes" => self.changes_in_block_by_type(request.params).await, "EXPERIMENTAL_changes_in_block" => self.changes_in_block(request.params).await, - "EXPERIMENTAL_check_tx" => self.check_tx(request.params).await, + "EXPERIMENTAL_check_tx" => { + let rpc_transaction_request = + near_jsonrpc_primitives::types::transactions::RpcBroadcastTransactionRequest::parse( + request.params, + )?; + let broadcast_tx_sync_response = self.check_tx(rpc_transaction_request).await?; + serde_json::to_value(broadcast_tx_sync_response) + .map_err(|err| RpcError::parse_error(err.to_string())) + } "EXPERIMENTAL_genesis_config" => self.genesis_config().await, "EXPERIMENTAL_light_client_proof" => { self.light_client_execution_outcome_proof(request.params).await @@ -321,7 +306,13 @@ impl JsonRpcHandler { let receipt = self.receipt(rpc_receipt_request).await?; serde_json::to_value(receipt).map_err(|err| RpcError::parse_error(err.to_string())) } - "EXPERIMENTAL_tx_status" => self.tx_status_common(request.params, true).await, + "EXPERIMENTAL_tx_status" => { + let rpc_transaction_status_common_request = near_jsonrpc_primitives::types::transactions::RpcTransactionStatusCommonRequest::parse(request.params)?; + let rpc_transaction_response = + self.tx_status_common(rpc_transaction_status_common_request, true).await?; + serde_json::to_value(rpc_transaction_response) + .map_err(|err| RpcError::parse_error(err.to_string())) + } "EXPERIMENTAL_validators_ordered" => self.validators_ordered(request.params).await, "gas_price" => { let rpc_gas_price_request = @@ -343,7 +334,13 @@ impl JsonRpcHandler { process_query_response(query_response) } "status" => self.status().await, - "tx" => self.tx_status_common(request.params, false).await, + "tx" => { + let rpc_transaction_status_common_request = near_jsonrpc_primitives::types::transactions::RpcTransactionStatusCommonRequest::parse(request.params)?; + let rpc_transaction_response = + self.tx_status_common(rpc_transaction_status_common_request, false).await?; + serde_json::to_value(rpc_transaction_response) + .map_err(|err| RpcError::parse_error(err.to_string())) + } "validators" => { let rpc_validator_request = near_jsonrpc_primitives::types::validator::RpcValidatorRequest::parse( @@ -366,22 +363,25 @@ impl JsonRpcHandler { response } - async fn send_tx_async(&self, params: Option) -> Result { - let tx = parse_tx(params)?; - let hash = (&tx.get_hash()).to_base(); + async fn send_tx_async( + &self, + request_data: near_jsonrpc_primitives::types::transactions::RpcBroadcastTransactionRequest, + ) -> CryptoHash { + let tx = request_data.signed_transaction; + let hash = tx.get_hash().clone(); self.client_addr.do_send(NetworkClientMessages::Transaction { transaction: tx, is_forwarded: false, - check_only: false, + check_only: false, // if we set true here it will not actually send the transaction }); - Ok(Value::String(hash)) + hash } async fn tx_exists( &self, tx_hash: CryptoHash, signer_account_id: &AccountId, - ) -> Result { + ) -> Result { timeout(self.polling_config.polling_timeout, async { loop { // TODO(optimization): Introduce a view_client method to only get transaction @@ -401,7 +401,9 @@ impl JsonRpcHandler { Ok(Err(TxStatusError::MissingTransaction(_))) => { return Ok(false); } - Err(_) => return Err(ServerError::InternalError), + Err(err) => return Err(near_jsonrpc_primitives::types::transactions::RpcTransactionError::InternalError { + debug_info: format!("{:?}", err) + }), _ => {} } sleep(self.polling_config.polling_interval).await; @@ -415,18 +417,23 @@ impl JsonRpcHandler { tx_hash, signer_account_id ); - ServerError::Timeout + near_jsonrpc_primitives::types::transactions::RpcTransactionError::TimeoutError })? } async fn tx_status_fetch( &self, - tx_info: TransactionInfo, + tx_info: near_jsonrpc_primitives::types::transactions::TransactionInfo, fetch_receipt: bool, ) -> Result { let (tx_hash, account_id) = match &tx_info { - TransactionInfo::Transaction(tx) => (tx.get_hash(), tx.transaction.signer_id.clone()), - TransactionInfo::TransactionId { hash, account_id } => (*hash, account_id.clone()), + near_jsonrpc_primitives::types::transactions::TransactionInfo::Transaction(tx) => { + (tx.get_hash(), tx.transaction.signer_id.clone()) + } + near_jsonrpc_primitives::types::transactions::TransactionInfo::TransactionId { + hash, + account_id, + } => (*hash, account_id.clone()), }; timeout(self.polling_config.polling_timeout, async { loop { @@ -440,9 +447,9 @@ impl JsonRpcHandler { .await; match tx_status_result { Ok(Ok(Some(outcome))) => break Ok(outcome), - Ok(Ok(None)) => {} + Ok(Ok(None)) => {} // No such transaction recorded on chain yet Ok(Err(err @ TxStatusError::MissingTransaction(_))) => { - if let TransactionInfo::Transaction(tx) = &tx_info { + if let near_jsonrpc_primitives::types::transactions::TransactionInfo::Transaction(tx) = &tx_info { if let Ok(NetworkClientResponses::InvalidTx(e)) = self.send_tx(tx.clone(), true).await { @@ -452,7 +459,7 @@ impl JsonRpcHandler { break Err(err); } Ok(Err(err)) => break Err(err), - Err(_) => break Err(TxStatusError::InternalError), + Err(err) => break Err(TxStatusError::InternalError(err.to_string())), } let _ = sleep(self.polling_config.polling_interval).await; } @@ -469,16 +476,28 @@ impl JsonRpcHandler { })? } - async fn tx_polling(&self, tx_info: TransactionInfo) -> Result { + async fn tx_polling( + &self, + tx_info: near_jsonrpc_primitives::types::transactions::TransactionInfo, + ) -> Result< + near_jsonrpc_primitives::types::transactions::RpcTransactionResponse, + near_jsonrpc_primitives::types::transactions::RpcTransactionError, + > { timeout(self.polling_config.polling_timeout, async { loop { match self.tx_status_fetch(tx_info.clone(), false).await { - Ok(tx_status) => break jsonify(Ok(Ok(tx_status))), + Ok(tx_status) => { + break Ok( + near_jsonrpc_primitives::types::transactions::RpcTransactionResponse { + final_execution_outcome: tx_status, + }, + ) + } // If transaction is missing, keep polling. Err(TxStatusError::MissingTransaction(_)) => {} // If we hit any other error, we return to the user. Err(err) => { - break jsonify::(Ok(Err(err.into()))); + break Err(err.into()); } } let _ = sleep(self.polling_config.polling_interval).await; @@ -491,7 +510,7 @@ impl JsonRpcHandler { target: "jsonrpc", "Timeout: tx_polling method. tx_info {:?}", tx_info, ); - timeout_err() + near_jsonrpc_primitives::types::transactions::RpcTransactionError::TimeoutError })? } @@ -502,7 +521,10 @@ impl JsonRpcHandler { &self, tx: SignedTransaction, check_only: bool, - ) -> Result { + ) -> Result< + NetworkClientResponses, + near_jsonrpc_primitives::types::transactions::RpcTransactionError, + > { let tx_hash = tx.get_hash(); let signer_account_id = tx.transaction.signer_id.clone(); let response = self @@ -512,7 +534,6 @@ impl JsonRpcHandler { is_forwarded: false, check_only, }) - .map_err(|err| RpcError::server_error(Some(ServerError::from(err)))) .await?; // If we receive InvalidNonce error, it might be the case that the transaction was @@ -530,79 +551,96 @@ impl JsonRpcHandler { Ok(response) } - async fn send_tx_sync(&self, params: Option) -> Result { - self.send_or_check_tx(params, false).await - } - - async fn check_tx(&self, params: Option) -> Result { - self.send_or_check_tx(params, true).await - } - - async fn send_or_check_tx( + async fn send_tx_sync( &self, - params: Option, - check_only: bool, - ) -> Result { - let tx = parse_tx(params)?; - let tx_hash = (&tx.get_hash()).to_base(); - let does_not_track_shard_err = - "Node doesn't track this shard. Cannot determine whether the transaction is valid"; - match self.send_tx(tx, check_only).await? { + request_data: near_jsonrpc_primitives::types::transactions::RpcBroadcastTransactionRequest, + ) -> Result< + near_jsonrpc_primitives::types::transactions::RpcBroadcastTxSyncResponse, + near_jsonrpc_primitives::types::transactions::RpcTransactionError, + > { + match self.send_tx(request_data.clone().signed_transaction, false).await? { NetworkClientResponses::ValidTx => { - if check_only { - Ok(Value::Null) - } else { - jsonify(Ok(Ok(RpcBroadcastTxSyncResponse { - transaction_hash: tx_hash, - is_routed: false, - }))) - } + Ok(near_jsonrpc_primitives::types::transactions::RpcBroadcastTxSyncResponse { + transaction_hash: (request_data.signed_transaction.get_hash()).to_string(), + }) } NetworkClientResponses::RequestRouted => { - if check_only { - Err(RpcError::server_error(Some(does_not_track_shard_err.to_string()))) - } else { - jsonify(Ok(Ok(RpcBroadcastTxSyncResponse { - transaction_hash: tx_hash, - is_routed: true, - }))) - } - } - NetworkClientResponses::InvalidTx(err) => { - Err(RpcError::server_error(Some(ServerError::TxExecutionError(err.into())))) + Err(near_jsonrpc_primitives::types::transactions::RpcTransactionError::RequestRouted { + transaction_hash: (request_data.signed_transaction.get_hash()).to_string(), + }) } - NetworkClientResponses::DoesNotTrackShard => { - Err(RpcError::server_error(Some(does_not_track_shard_err.to_string()))) + network_client_responses=> Err( + near_jsonrpc_primitives::types::transactions::RpcTransactionError::from_network_client_responses( + network_client_responses + ) + ) + } + } + + async fn check_tx( + &self, + request_data: near_jsonrpc_primitives::types::transactions::RpcBroadcastTransactionRequest, + ) -> Result< + near_jsonrpc_primitives::types::transactions::RpcBroadcastTxSyncResponse, + near_jsonrpc_primitives::types::transactions::RpcTransactionError, + > { + match self.send_tx(request_data.clone().signed_transaction, true).await? { + NetworkClientResponses::ValidTx => { + Ok(near_jsonrpc_primitives::types::transactions::RpcBroadcastTxSyncResponse { + transaction_hash: (request_data.signed_transaction.get_hash()).to_string(), + }) } - _ => { - // this is only possible if something went wrong with the node internally. - Err(RpcError::server_error(Some(ServerError::InternalError))) + NetworkClientResponses::RequestRouted => { + Err(near_jsonrpc_primitives::types::transactions::RpcTransactionError::RequestRouted { + transaction_hash: (request_data.signed_transaction.get_hash()).to_string(), + }) } + network_client_responses => Err( + near_jsonrpc_primitives::types::transactions::RpcTransactionError::from_network_client_responses(network_client_responses) + ) } } - async fn send_tx_commit(&self, params: Option) -> Result { - let tx = parse_tx(params)?; - match self.tx_status_fetch(TransactionInfo::Transaction(tx.clone()), false).await { + async fn send_tx_commit( + &self, + request_data: near_jsonrpc_primitives::types::transactions::RpcBroadcastTransactionRequest, + ) -> Result< + near_jsonrpc_primitives::types::transactions::RpcTransactionResponse, + near_jsonrpc_primitives::types::transactions::RpcTransactionError, + > { + let tx = request_data.signed_transaction; + match self + .tx_status_fetch( + near_jsonrpc_primitives::types::transactions::TransactionInfo::Transaction( + tx.clone(), + ), + false, + ) + .await + { Ok(outcome) => { - return jsonify(Ok(Ok(outcome))); + return Ok(near_jsonrpc_primitives::types::transactions::RpcTransactionResponse { + final_execution_outcome: outcome, + }); } - Err(TxStatusError::InvalidTx(e)) => { - return Err(RpcError::server_error(Some(ServerError::TxExecutionError(e.into())))); + Err(TxStatusError::InvalidTx(invalid_tx_error)) => { + return Err(near_jsonrpc_primitives::types::transactions::RpcTransactionError::InvalidTransaction { + context: invalid_tx_error + }); } _ => {} } match self.send_tx(tx.clone(), false).await? { NetworkClientResponses::ValidTx | NetworkClientResponses::RequestRouted => { - self.tx_polling(TransactionInfo::Transaction(tx)).await - } - NetworkClientResponses::InvalidTx(err) => { - Err(RpcError::server_error(Some(ServerError::TxExecutionError(err.into())))) + self.tx_polling(near_jsonrpc_primitives::types::transactions::TransactionInfo::Transaction(tx)).await } - NetworkClientResponses::NoResponse => { - Err(RpcError::server_error(Some(ServerError::Timeout))) + network_client_response=> { + Err( + near_jsonrpc_primitives::types::transactions::RpcTransactionError::from_network_client_responses( + network_client_response + ) + ) } - _ => Err(RpcError::server_error(Some(ServerError::InternalError))), } } @@ -657,27 +695,13 @@ impl JsonRpcHandler { async fn tx_status_common( &self, - params: Option, + request_data: near_jsonrpc_primitives::types::transactions::RpcTransactionStatusCommonRequest, fetch_receipt: bool, - ) -> Result { - let tx_status_request = - if let Ok((hash, account_id)) = parse_params::<(CryptoHash, String)>(params.clone()) { - if !is_valid_account_id(&account_id) { - return Err(RpcError::invalid_params(format!( - "Invalid account id: {}", - account_id - ))); - } - TransactionInfo::TransactionId { hash, account_id } - } else { - let tx = parse_tx(params)?; - TransactionInfo::Transaction(tx) - }; - - jsonify(Ok(self - .tx_status_fetch(tx_status_request, fetch_receipt) - .await - .map_err(|err| err.into()))) + ) -> Result< + near_jsonrpc_primitives::types::transactions::RpcTransactionResponse, + near_jsonrpc_primitives::types::transactions::RpcTransactionError, + > { + Ok(self.tx_status_fetch(request_data.transaction_info, fetch_receipt).await?.into()) } async fn block( @@ -789,7 +813,7 @@ impl JsonRpcHandler { .view_client_addr .send(GetExecutionOutcome { id }) .await - .map_err(|e| RpcError::from(ServerError::from(e)))? + .map_err(|e| RpcError::from(near_jsonrpc_primitives::errors::ServerError::from(e)))? .map_err(|e| RpcError::server_error(Some(e)))?; let block_proof = self .view_client_addr @@ -798,7 +822,7 @@ impl JsonRpcHandler { head_block_hash: light_client_head, }) .await - .map_err(|e| RpcError::from(ServerError::from(e)))?; + .map_err(|e| RpcError::from(near_jsonrpc_primitives::errors::ServerError::from(e)))?; let res = block_proof.map(|block_proof| RpcLightClientExecutionProofResponse { outcome_proof: execution_outcome_proof.outcome_proof, outcome_root_proof: execution_outcome_proof.outcome_root_proof, diff --git a/chain/jsonrpc/tests/rpc_transactions.rs b/chain/jsonrpc/tests/rpc_transactions.rs index b5621a1993c..72f482ec502 100644 --- a/chain/jsonrpc/tests/rpc_transactions.rs +++ b/chain/jsonrpc/tests/rpc_transactions.rs @@ -137,7 +137,13 @@ fn test_expired_tx() { actix::spawn( client .broadcast_tx_commit(to_base64(&bytes)) - .map_err(|_| { + .map_err(|err| { + assert_eq!( + err.data.unwrap(), + serde_json::json!({"TxExecutionError": { + "InvalidTxError": "Expired" + }}) + ); System::current().stop(); }) .map(|_| ()), diff --git a/chain/rosetta-rpc/src/errors.rs b/chain/rosetta-rpc/src/errors.rs index eb310df1aab..06e6dfc1a96 100644 --- a/chain/rosetta-rpc/src/errors.rs +++ b/chain/rosetta-rpc/src/errors.rs @@ -37,7 +37,7 @@ impl std::convert::From for ErrorKind { "Transaction is invalid, so it will never be included to the chain: {:?}", err )), - near_client::TxStatusError::InternalError + near_client::TxStatusError::InternalError(_) | near_client::TxStatusError::TimeoutError => { // TODO: remove the statuses from TxStatusError since they are // never constructed by the view client (it is a leak of diff --git a/core/primitives/src/views.rs b/core/primitives/src/views.rs index 05f523a53db..9e49515710a 100644 --- a/core/primitives/src/views.rs +++ b/core/primitives/src/views.rs @@ -1051,7 +1051,7 @@ impl From for ExecutionOutcomeWithIdView { } } -#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] +#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Debug)] #[serde(untagged)] pub enum FinalExecutionOutcomeViewEnum { FinalExecutionOutcome(FinalExecutionOutcomeView), @@ -1087,7 +1087,7 @@ impl fmt::Debug for FinalExecutionOutcomeView { /// Final execution outcome of the transaction and all of subsequent the receipts. Also includes /// the generated receipt. -#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] pub struct FinalExecutionOutcomeWithReceiptView { /// Final outcome view without receipts #[serde(flatten)] diff --git a/neard/tests/rpc_nodes.rs b/neard/tests/rpc_nodes.rs index e3ba649a7b8..5d89cabd0ed 100644 --- a/neard/tests/rpc_nodes.rs +++ b/neard/tests/rpc_nodes.rs @@ -939,6 +939,7 @@ fn test_send_tx_sync_to_lightclient_must_be_routed() { ); let client = new_client(&format!("http://{}", rpc_addrs[1])); + let tx_hash = transaction.get_hash(); let bytes = transaction.try_to_vec().unwrap(); actix::spawn(async move { @@ -949,12 +950,16 @@ fn test_send_tx_sync_to_lightclient_must_be_routed() { let _ = client .EXPERIMENTAL_broadcast_tx_sync(to_base64(&bytes)) .map_err(|err| { - panic_on_rpc_error!(err); - }) - .map_ok(move |response| { - assert_eq!(response["is_routed"], true); + assert_eq!( + err.data.unwrap(), + serde_json::json!(format!( + "Transaction with hash {} was routed", + tx_hash + )) + ); System::current().stop(); }) + .map_ok(move |_| panic!("Transaction must not succeed")) .await; break; } @@ -1003,9 +1008,13 @@ fn test_check_unknown_tx_must_return_error() { let _ = client .EXPERIMENTAL_tx_status(to_base64(&bytes)) .map_err(|err| { - assert!(err.data.unwrap().to_string().contains( - format!("Transaction {} doesn't exist", tx_hash).as_str() - )); + assert_eq!( + err.data.unwrap(), + serde_json::json!(format!( + "Transaction {} doesn't exist", + tx_hash + )) + ); System::current().stop(); }) .map_ok(move |_| panic!("Transaction must be unknown")) @@ -1060,8 +1069,8 @@ fn test_check_tx_on_lightclient_must_return_does_not_track_shard() { .EXPERIMENTAL_check_tx(to_base64(&bytes)) .map_err(|err| { assert_eq!( - err.data.unwrap().to_string(), - "\"Node doesn\'t track this shard. Cannot determine whether the transaction is valid\"".to_string() + err.data.unwrap(), + serde_json::json!("Node doesn't track this shard. Cannot determine whether the transaction is valid") ); System::current().stop(); }) diff --git a/test-utils/testlib/src/node/mod.rs b/test-utils/testlib/src/node/mod.rs index 97f1a456ba8..8cfb040fe49 100644 --- a/test-utils/testlib/src/node/mod.rs +++ b/test-utils/testlib/src/node/mod.rs @@ -3,7 +3,7 @@ use std::sync::RwLock; use near_chain_configs::Genesis; use near_crypto::{InMemorySigner, Signer}; -use near_jsonrpc::ServerError; +use near_jsonrpc_primitives::errors::ServerError; use near_primitives::state_record::StateRecord; use near_primitives::transaction::SignedTransaction; use near_primitives::types::{AccountId, Balance, NumSeats}; diff --git a/test-utils/testlib/src/standard_test_cases.rs b/test-utils/testlib/src/standard_test_cases.rs index 85916f0e358..eef765309cb 100644 --- a/test-utils/testlib/src/standard_test_cases.rs +++ b/test-utils/testlib/src/standard_test_cases.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use assert_matches::assert_matches; use near_crypto::{InMemorySigner, KeyType}; -use near_jsonrpc::ServerError; +use near_jsonrpc_primitives::errors::ServerError; use near_primitives::account::{AccessKey, AccessKeyPermission, FunctionCallPermission}; use near_primitives::errors::{ ActionError, ActionErrorKind, InvalidAccessKeyError, InvalidTxError, TxExecutionError, diff --git a/test-utils/testlib/src/user/mod.rs b/test-utils/testlib/src/user/mod.rs index 7cdbf34a849..d0b9dd6ce64 100644 --- a/test-utils/testlib/src/user/mod.rs +++ b/test-utils/testlib/src/user/mod.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use futures::{future::LocalBoxFuture, FutureExt}; use near_crypto::{PublicKey, Signer}; -use near_jsonrpc::ServerError; +use near_jsonrpc_primitives::errors::ServerError; use near_primitives::account::AccessKey; use near_primitives::hash::CryptoHash; use near_primitives::receipt::Receipt; diff --git a/test-utils/testlib/src/user/rpc_user.rs b/test-utils/testlib/src/user/rpc_user.rs index 5cabc8a364a..64b932ada6d 100644 --- a/test-utils/testlib/src/user/rpc_user.rs +++ b/test-utils/testlib/src/user/rpc_user.rs @@ -8,8 +8,8 @@ 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_jsonrpc_client::ChunkId; +use near_jsonrpc_primitives::errors::ServerError; use near_jsonrpc_primitives::types::query::RpcQueryResponse; use near_primitives::hash::CryptoHash; use near_primitives::receipt::Receipt; diff --git a/test-utils/testlib/src/user/runtime_user.rs b/test-utils/testlib/src/user/runtime_user.rs index 5211f9b641c..4df1e6c64cd 100644 --- a/test-utils/testlib/src/user/runtime_user.rs +++ b/test-utils/testlib/src/user/runtime_user.rs @@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet}; use std::sync::{Arc, RwLock}; use near_crypto::{PublicKey, Signer}; -use near_jsonrpc::ServerError; +use near_jsonrpc_primitives::errors::ServerError; use near_primitives::errors::{RuntimeError, TxExecutionError}; use near_primitives::hash::CryptoHash; use near_primitives::receipt::Receipt;