From 8c3a09ec5208b08a99c071ec37f84cd2467c6cec Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Mon, 18 Nov 2024 12:11:30 +0100 Subject: [PATCH 1/9] initial error implemetnation based on network/options --- src/error.rs | 110 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 98 insertions(+), 12 deletions(-) diff --git a/src/error.rs b/src/error.rs index fc9588a..070f5b1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,11 +1,12 @@ use std::num::ParseIntError; use cynic::http::CynicReqwestError; -use serde_json::Error as SerdeError; +use serde::Serialize; +use serde_json::{json, Error as SerdeError}; use sqlx::Error as SqlxError; use thiserror::Error; -#[derive(Error, Debug, PartialEq)] +#[derive(Error, Debug, PartialEq, Serialize)] pub enum MinaMeshError { #[error("SQL failure: {0}")] Sql(String), @@ -13,7 +14,7 @@ pub enum MinaMeshError { #[error("JSON parse error")] JsonParse(Option), - #[error("GraphQL query failed")] + #[error("GraphQL query failed: {0}")] GraphqlMinaQuery(String), #[error("Network doesn't exist")] @@ -86,7 +87,7 @@ pub enum MinaMeshError { TransactionSubmitExpired, } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Serialize)] pub enum PartialReason { LengthMismatch, FeePayerAndSourceMismatch, @@ -101,26 +102,111 @@ pub enum PartialReason { } impl MinaMeshError { + /// Returns the error code for the error. pub fn error_code(&self) -> u8 { - unimplemented!(); + match self { + MinaMeshError::Sql(_) => 1, + MinaMeshError::JsonParse(_) => 2, + MinaMeshError::GraphqlMinaQuery(_) => 3, + MinaMeshError::NetworkDne(_, _) => 4, + MinaMeshError::ChainInfoMissing => 5, + MinaMeshError::AccountNotFound(_) => 6, + MinaMeshError::InvariantViolation => 7, + MinaMeshError::TransactionNotFound(_) => 8, + MinaMeshError::BlockMissing(_) => 9, + MinaMeshError::MalformedPublicKey => 10, + MinaMeshError::OperationsNotValid(_) => 11, + MinaMeshError::UnsupportedOperationForConstruction => 12, + MinaMeshError::SignatureMissing => 13, + MinaMeshError::PublicKeyFormatNotValid => 14, + MinaMeshError::NoOptionsProvided => 15, + MinaMeshError::Exception(_) => 16, + MinaMeshError::SignatureInvalid => 17, + MinaMeshError::MemoInvalid => 18, + MinaMeshError::GraphqlUriNotSet => 19, + MinaMeshError::TransactionSubmitNoSender => 20, + MinaMeshError::TransactionSubmitDuplicate => 21, + MinaMeshError::TransactionSubmitBadNonce => 22, + MinaMeshError::TransactionSubmitFeeSmall => 23, + MinaMeshError::TransactionSubmitInvalidSignature => 24, + MinaMeshError::TransactionSubmitInsufficientBalance => 25, + MinaMeshError::TransactionSubmitExpired => 26, + } } - pub fn description(&self) -> String { + /// Returns whether the error is retriable. + pub fn is_retriable(&self) -> bool { + matches!( + self, + MinaMeshError::GraphqlMinaQuery(_) + | MinaMeshError::TransactionSubmitNoSender + | MinaMeshError::AccountNotFound(_) + | MinaMeshError::TransactionNotFound(_) + | MinaMeshError::BlockMissing(_) + | MinaMeshError::ChainInfoMissing + ) + } + + /// Provides additional details about the error. + pub fn details(&self) -> serde_json::Value { match self { - MinaMeshError::Sql(s) => s.clone(), - _ => unimplemented!(), + MinaMeshError::GraphqlMinaQuery(msg) => json!({ + "error": msg, + "extra": "Internal POST to Mina Daemon failed" + }), + MinaMeshError::Sql(msg) => json!({ "error": msg }), + MinaMeshError::JsonParse(Some(msg)) => json!({ "error": msg }), + _ => json!(null), } } - pub fn is_retriable(&self) -> bool { - unimplemented!(); + /// Converts the error into a JSON representation. + pub fn to_json(&self) -> serde_json::Value { + json!({ + "code": self.error_code(), + "message": self.to_string(), + "description": self.description(), + "retriable": self.is_retriable(), + "details": self.details(), + }) } - pub fn context(&self) -> Option { - unimplemented!(); + /// Returns a human-readable description of the error. + pub fn description(&self) -> String { + match self { + MinaMeshError::Sql(_) => "An SQL error occurred.".to_string(), + MinaMeshError::JsonParse(_) => "Failed to parse JSON.".to_string(), + MinaMeshError::GraphqlMinaQuery(_) => "The GraphQL query failed.".to_string(), + MinaMeshError::NetworkDne(_, _) => "The specified network does not exist.".to_string(), + MinaMeshError::ChainInfoMissing => "Chain info is missing.".to_string(), + MinaMeshError::AccountNotFound(_) => "The specified account could not be found.".to_string(), + MinaMeshError::InvariantViolation => "An internal invariant was violated.".to_string(), + MinaMeshError::TransactionNotFound(_) => "The specified transaction could not be found.".to_string(), + MinaMeshError::BlockMissing(_) => "The specified block could not be found.".to_string(), + MinaMeshError::MalformedPublicKey => "The provided public key is malformed.".to_string(), + MinaMeshError::OperationsNotValid(_) => "The provided operations are not valid.".to_string(), + MinaMeshError::UnsupportedOperationForConstruction => { + "The operation is not supported for transaction construction.".to_string() + } + MinaMeshError::SignatureMissing => "A signature is missing.".to_string(), + MinaMeshError::PublicKeyFormatNotValid => "The public key format is not valid.".to_string(), + MinaMeshError::NoOptionsProvided => "No options were provided.".to_string(), + MinaMeshError::Exception(_) => "An internal exception occurred.".to_string(), + MinaMeshError::SignatureInvalid => "The signature is invalid.".to_string(), + MinaMeshError::MemoInvalid => "The memo is invalid.".to_string(), + MinaMeshError::GraphqlUriNotSet => "No GraphQL URI has been set.".to_string(), + MinaMeshError::TransactionSubmitNoSender => "No sender was found in the ledger.".to_string(), + MinaMeshError::TransactionSubmitDuplicate => "A duplicate transaction was detected.".to_string(), + MinaMeshError::TransactionSubmitBadNonce => "The nonce is invalid.".to_string(), + MinaMeshError::TransactionSubmitFeeSmall => "The transaction fee is too small.".to_string(), + MinaMeshError::TransactionSubmitInvalidSignature => "The transaction signature is invalid.".to_string(), + MinaMeshError::TransactionSubmitInsufficientBalance => "The account has insufficient balance.".to_string(), + MinaMeshError::TransactionSubmitExpired => "The transaction has expired.".to_string(), + } } } +/// Implement `From` conversions for third-party errors. impl From for MinaMeshError { fn from(value: SqlxError) -> Self { MinaMeshError::Sql(value.to_string()) From 5c8ae55e7d8cbd39f3fe9723bd5c192cf025b8df Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Mon, 18 Nov 2024 14:50:14 +0100 Subject: [PATCH 2/9] adjust Wrapper and handler macro --- src/commands/serve.rs | 21 +++++++++++--------- src/error.rs | 46 ++++++++++++++++++++++++++++++++++++++++--- src/util.rs | 11 ++++++++--- 3 files changed, 63 insertions(+), 15 deletions(-) diff --git a/src/commands/serve.rs b/src/commands/serve.rs index 845f732..a5e67ac 100644 --- a/src/commands/serve.rs +++ b/src/commands/serve.rs @@ -12,7 +12,7 @@ use clap::Args; use paste::paste; use tokio::net::TcpListener; -use crate::{playground::handle_playground, util::Wrapper, MinaMesh, MinaMeshConfig}; +use crate::{playground::handle_playground, util::Wrapper, MinaMesh, MinaMeshConfig, MinaMeshError}; #[derive(Debug, Args)] #[command(about = "Start the Mina Mesh Server.")] @@ -64,18 +64,21 @@ impl ServeCommand { macro_rules! create_handler { ($name:ident, $request_type:ty) => { - paste! { - async fn [](mina_mesh: State>, Json(req): Json) -> impl IntoResponse { - Wrapper(mina_mesh.$name(req).await) + paste! { + async fn [](mina_mesh: State>, req: Result, axum::extract::rejection::JsonRejection>) -> impl IntoResponse { + match req { + Ok(Json(req)) => Wrapper(mina_mesh.$name(req).await.map_err(MinaMeshError::from)), // Normalize errors to MinaMeshError + Err(err) => Wrapper(Err(MinaMeshError::from(err))), // Convert JsonRejection to MinaMeshError + } + } } - } }; ($name:ident) => { - paste! { - async fn [](mina_mesh: State>) -> impl IntoResponse { - Wrapper(mina_mesh.$name().await) + paste! { + async fn [](mina_mesh: State>) -> impl IntoResponse { + Wrapper(mina_mesh.$name().await.map_err(MinaMeshError::from)) // Normalize errors to MinaMeshError + } } - } }; } diff --git a/src/error.rs b/src/error.rs index 070f5b1..a8f0a4b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,12 +1,18 @@ use std::num::ParseIntError; +use axum::{ + extract::rejection::JsonRejection, + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; use cynic::http::CynicReqwestError; use serde::Serialize; use serde_json::{json, Error as SerdeError}; use sqlx::Error as SqlxError; use thiserror::Error; -#[derive(Error, Debug, PartialEq, Serialize)] +#[derive(Error, Debug, PartialEq, Serialize, Clone)] pub enum MinaMeshError { #[error("SQL failure: {0}")] Sql(String), @@ -87,7 +93,7 @@ pub enum MinaMeshError { TransactionSubmitExpired, } -#[derive(Debug, PartialEq, Serialize)] +#[derive(Clone, Debug, PartialEq, Serialize)] pub enum PartialReason { LengthMismatch, FeePayerAndSourceMismatch, @@ -156,7 +162,7 @@ impl MinaMeshError { }), MinaMeshError::Sql(msg) => json!({ "error": msg }), MinaMeshError::JsonParse(Some(msg)) => json!({ "error": msg }), - _ => json!(null), + _ => json!(""), } } @@ -206,6 +212,21 @@ impl MinaMeshError { } } +impl IntoResponse for MinaMeshError { + fn into_response(self) -> Response { + let status_code = StatusCode::BAD_REQUEST; + let body = json!({ + "code": self.error_code(), + "message": self.to_string(), + "description": self.description(), + "retriable": self.is_retriable(), + "details": self.details(), + }); + + (status_code, Json(body)).into_response() + } +} + /// Implement `From` conversions for third-party errors. impl From for MinaMeshError { fn from(value: SqlxError) -> Self { @@ -232,3 +253,22 @@ impl From for MinaMeshError { MinaMeshError::JsonParse(Some(value.to_string())) } } + +impl From for MinaMeshError { + fn from(error: anyhow::Error) -> Self { + if let Some(mina_error) = error.downcast_ref::() { + // Clone the original MinaMeshError if it exists + (*mina_error).clone() + } else { + // Fallback to wrapping as Exception if it's not a MinaMeshError + MinaMeshError::Exception(error.to_string()) + } + } +} + +/// Convert Axum's JsonRejection into MinaMeshError. +impl From for MinaMeshError { + fn from(err: JsonRejection) -> Self { + MinaMeshError::JsonParse(Some(err.to_string())) + } +} diff --git a/src/util.rs b/src/util.rs index d5a949d..a3cb9bf 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,7 +1,6 @@ use anyhow::Result; use axum::{ extract::Json, - http::StatusCode, response::{IntoResponse, Response}, }; use serde::Serialize; @@ -10,11 +9,17 @@ use crate::MinaMeshError; pub struct Wrapper(pub T); -impl IntoResponse for Wrapper> { +impl IntoResponse for Wrapper> +where + MinaMeshError: From, +{ fn into_response(self) -> Response { match self.0 { Ok(v) => Json(v).into_response(), - Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), + Err(err) => { + let mina_error: MinaMeshError = err.into(); + mina_error.into_response() + } } } } From 2291f0dcb7edd9d9f3c132f7c6a252162bec262e Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Mon, 18 Nov 2024 17:56:21 +0100 Subject: [PATCH 3/9] update status codes --- src/error.rs | 46 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/src/error.rs b/src/error.rs index a8f0a4b..89f0a03 100644 --- a/src/error.rs +++ b/src/error.rs @@ -160,8 +160,14 @@ impl MinaMeshError { "error": msg, "extra": "Internal POST to Mina Daemon failed" }), - MinaMeshError::Sql(msg) => json!({ "error": msg }), - MinaMeshError::JsonParse(Some(msg)) => json!({ "error": msg }), + MinaMeshError::Sql(msg) => json!({ + "error": msg, + "extra": "Internal SQL query failed" + }), + MinaMeshError::JsonParse(Some(msg)) => json!({ + "error": msg, + "extra": "Failed to parse JSON body" + }), _ => json!(""), } } @@ -181,7 +187,7 @@ impl MinaMeshError { pub fn description(&self) -> String { match self { MinaMeshError::Sql(_) => "An SQL error occurred.".to_string(), - MinaMeshError::JsonParse(_) => "Failed to parse JSON.".to_string(), + MinaMeshError::JsonParse(_) => "We encountered an error while parsing JSON.".to_string(), MinaMeshError::GraphqlMinaQuery(_) => "The GraphQL query failed.".to_string(), MinaMeshError::NetworkDne(_, _) => "The specified network does not exist.".to_string(), MinaMeshError::ChainInfoMissing => "Chain info is missing.".to_string(), @@ -214,7 +220,35 @@ impl MinaMeshError { impl IntoResponse for MinaMeshError { fn into_response(self) -> Response { - let status_code = StatusCode::BAD_REQUEST; + let status_code = match self { + MinaMeshError::Sql(_) => StatusCode::INTERNAL_SERVER_ERROR, + MinaMeshError::JsonParse(_) => StatusCode::BAD_REQUEST, + MinaMeshError::GraphqlMinaQuery(_) => StatusCode::BAD_GATEWAY, + MinaMeshError::NetworkDne(_, _) => StatusCode::NOT_FOUND, + MinaMeshError::ChainInfoMissing => StatusCode::INTERNAL_SERVER_ERROR, + MinaMeshError::AccountNotFound(_) => StatusCode::NOT_FOUND, + MinaMeshError::InvariantViolation => StatusCode::INTERNAL_SERVER_ERROR, + MinaMeshError::TransactionNotFound(_) => StatusCode::NOT_FOUND, + MinaMeshError::BlockMissing(_) => StatusCode::NOT_FOUND, + MinaMeshError::MalformedPublicKey => StatusCode::BAD_REQUEST, + MinaMeshError::OperationsNotValid(_) => StatusCode::BAD_REQUEST, + MinaMeshError::UnsupportedOperationForConstruction => StatusCode::BAD_REQUEST, + MinaMeshError::SignatureMissing => StatusCode::BAD_REQUEST, + MinaMeshError::PublicKeyFormatNotValid => StatusCode::BAD_REQUEST, + MinaMeshError::NoOptionsProvided => StatusCode::BAD_REQUEST, + MinaMeshError::Exception(_) => StatusCode::INTERNAL_SERVER_ERROR, + MinaMeshError::SignatureInvalid => StatusCode::BAD_REQUEST, + MinaMeshError::MemoInvalid => StatusCode::BAD_REQUEST, + MinaMeshError::GraphqlUriNotSet => StatusCode::INTERNAL_SERVER_ERROR, + MinaMeshError::TransactionSubmitNoSender => StatusCode::BAD_REQUEST, + MinaMeshError::TransactionSubmitDuplicate => StatusCode::CONFLICT, + MinaMeshError::TransactionSubmitBadNonce => StatusCode::BAD_REQUEST, + MinaMeshError::TransactionSubmitFeeSmall => StatusCode::BAD_REQUEST, + MinaMeshError::TransactionSubmitInvalidSignature => StatusCode::BAD_REQUEST, + MinaMeshError::TransactionSubmitInsufficientBalance => StatusCode::BAD_REQUEST, + MinaMeshError::TransactionSubmitExpired => StatusCode::BAD_REQUEST, + }; + let body = json!({ "code": self.error_code(), "message": self.to_string(), @@ -258,7 +292,7 @@ impl From for MinaMeshError { fn from(error: anyhow::Error) -> Self { if let Some(mina_error) = error.downcast_ref::() { // Clone the original MinaMeshError if it exists - (*mina_error).clone() + mina_error.clone() } else { // Fallback to wrapping as Exception if it's not a MinaMeshError MinaMeshError::Exception(error.to_string()) @@ -269,6 +303,6 @@ impl From for MinaMeshError { /// Convert Axum's JsonRejection into MinaMeshError. impl From for MinaMeshError { fn from(err: JsonRejection) -> Self { - MinaMeshError::JsonParse(Some(err.to_string())) + MinaMeshError::JsonParse(Some(err.body_text())) } } From 98252fcce15e54088af097cf19a2f5ffce80b8db Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Tue, 19 Nov 2024 13:14:50 +0100 Subject: [PATCH 4/9] testing --- src/api/network_list.rs | 4 +- src/config.rs | 8 +- src/error.rs | 6 +- tests/error.rs | 194 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 207 insertions(+), 5 deletions(-) create mode 100644 tests/error.rs diff --git a/src/api/network_list.rs b/src/api/network_list.rs index 4148cb0..6c06dd5 100644 --- a/src/api/network_list.rs +++ b/src/api/network_list.rs @@ -2,11 +2,11 @@ use anyhow::Result; use coinbase_mesh::models::{NetworkIdentifier, NetworkListResponse}; use cynic::QueryBuilder; -use crate::{graphql::QueryNetworkId, MinaMesh}; +use crate::{graphql::QueryNetworkId, MinaMesh, MinaMeshError}; /// https://github.com/MinaProtocol/mina/blob/985eda49bdfabc046ef9001d3c406e688bc7ec45/src/app/rosetta/lib/network.ml#L162 impl MinaMesh { - pub async fn network_list(&self) -> Result { + pub async fn network_list(&self) -> Result { let QueryNetworkId { network_id } = self.graphql_client.send(QueryNetworkId::build(())).await?; let (chain_id, network_id) = network_id.split_once(':').map_or_else( || ("unknown".to_string(), "unknown".to_string()), diff --git a/src/config.rs b/src/config.rs index bd58e84..d779978 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,7 +5,7 @@ use clap::{Args, Parser}; use coinbase_mesh::models::BlockIdentifier; use sqlx::postgres::PgPoolOptions; -use crate::{graphql::GraphQLClient, util::default_mina_proxy_url, MinaMesh}; +use crate::{graphql::GraphQLClient, util::default_mina_proxy_url, MinaMesh, MinaMeshError}; #[derive(Debug, Args)] pub struct MinaMeshConfig { @@ -51,7 +51,11 @@ impl MinaMeshConfig { } } - pub async fn to_mina_mesh(self) -> Result { + pub async fn to_mina_mesh(self) -> Result { + if self.proxy_url.is_empty() { + return Err(MinaMeshError::GraphqlUriNotSet); + } + Ok(MinaMesh { graphql_client: GraphQLClient::new(self.proxy_url.to_owned()), pg_pool: PgPoolOptions::new() diff --git a/src/error.rs b/src/error.rs index 89f0a03..1e0dcbc 100644 --- a/src/error.rs +++ b/src/error.rs @@ -29,7 +29,7 @@ pub enum MinaMeshError { #[error("Chain info missing")] ChainInfoMissing, - #[error("Account not found")] + #[error("Account not found: {0}")] AccountNotFound(String), #[error("Internal invariant violation (you found a bug)")] @@ -168,6 +168,10 @@ impl MinaMeshError { "error": msg, "extra": "Failed to parse JSON body" }), + MinaMeshError::AccountNotFound(account) => json!({ + "error": format!("You attempted to lookup {}, but we couldn't find it in the ledger.", account), + "account": account, + }), _ => json!(""), } } diff --git a/tests/error.rs b/tests/error.rs new file mode 100644 index 0000000..f51d840 --- /dev/null +++ b/tests/error.rs @@ -0,0 +1,194 @@ +use std::env; + +// use axum::http::StatusCode; +use mina_mesh::{MinaMeshConfig, MinaMeshError}; +// use serde_json::json; + +#[test] +fn test_error_codes_and_descriptions() { + let error = MinaMeshError::Sql("SQL syntax error".to_string()); + assert_eq!(error.error_code(), 1); + assert_eq!(error.description(), "An SQL error occurred."); + assert!(!error.is_retriable()); + + let error = MinaMeshError::JsonParse(Some("Missing field".to_string())); + assert_eq!(error.error_code(), 2); + assert_eq!(error.description(), "We encountered an error while parsing JSON."); + assert!(!error.is_retriable()); + + let error = MinaMeshError::GraphqlMinaQuery("Timeout".to_string()); + assert_eq!(error.error_code(), 3); + assert_eq!(error.description(), "The GraphQL query failed."); + assert!(error.is_retriable()); + + let error = MinaMeshError::NetworkDne("blockchain".to_string(), "network".to_string()); + assert_eq!(error.error_code(), 4); + assert_eq!(error.description(), "The specified network does not exist."); + assert!(!error.is_retriable()); + + let error = MinaMeshError::ChainInfoMissing; + assert_eq!(error.error_code(), 5); + assert_eq!(error.description(), "Chain info is missing."); + assert!(error.is_retriable()); + + let error = MinaMeshError::AccountNotFound("Account ID".to_string()); + assert_eq!(error.error_code(), 6); + assert_eq!(error.description(), "The specified account could not be found."); + assert!(error.is_retriable()); + + let error = MinaMeshError::InvariantViolation; + assert_eq!(error.error_code(), 7); + assert_eq!(error.description(), "An internal invariant was violated."); + assert!(!error.is_retriable()); + + let error = MinaMeshError::TransactionNotFound("Transaction ID".to_string()); + assert_eq!(error.error_code(), 8); + assert_eq!(error.description(), "The specified transaction could not be found."); + assert!(error.is_retriable()); + + let error = MinaMeshError::BlockMissing("Block ID".to_string()); + assert_eq!(error.error_code(), 9); + assert_eq!(error.description(), "The specified block could not be found."); + assert!(error.is_retriable()); + + let error = MinaMeshError::MalformedPublicKey; + assert_eq!(error.error_code(), 10); + assert_eq!(error.description(), "The provided public key is malformed."); + assert!(!error.is_retriable()); + + let error = MinaMeshError::OperationsNotValid(vec![]); + assert_eq!(error.error_code(), 11); + assert_eq!(error.description(), "The provided operations are not valid."); + assert!(!error.is_retriable()); + + let error = MinaMeshError::UnsupportedOperationForConstruction; + assert_eq!(error.error_code(), 12); + assert_eq!(error.description(), "The operation is not supported for transaction construction."); + assert!(!error.is_retriable()); + + let error = MinaMeshError::SignatureMissing; + assert_eq!(error.error_code(), 13); + assert_eq!(error.description(), "A signature is missing."); + assert!(!error.is_retriable()); + + let error = MinaMeshError::PublicKeyFormatNotValid; + assert_eq!(error.error_code(), 14); + assert_eq!(error.description(), "The public key format is not valid."); + assert!(!error.is_retriable()); + + let error = MinaMeshError::NoOptionsProvided; + assert_eq!(error.error_code(), 15); + assert_eq!(error.description(), "No options were provided."); + assert!(!error.is_retriable()); + + let error = MinaMeshError::Exception("Unexpected error".to_string()); + assert_eq!(error.error_code(), 16); + assert_eq!(error.description(), "An internal exception occurred."); + assert!(!error.is_retriable()); + + let error = MinaMeshError::SignatureInvalid; + assert_eq!(error.error_code(), 17); + assert_eq!(error.description(), "The signature is invalid."); + assert!(!error.is_retriable()); + + let error = MinaMeshError::MemoInvalid; + assert_eq!(error.error_code(), 18); + assert_eq!(error.description(), "The memo is invalid."); + assert!(!error.is_retriable()); + + let error = MinaMeshError::GraphqlUriNotSet; + assert_eq!(error.error_code(), 19); + assert_eq!(error.description(), "No GraphQL URI has been set."); + assert!(!error.is_retriable()); + + let error = MinaMeshError::TransactionSubmitNoSender; + assert_eq!(error.error_code(), 20); + assert_eq!(error.description(), "No sender was found in the ledger."); + assert!(error.is_retriable()); + + let error = MinaMeshError::TransactionSubmitDuplicate; + assert_eq!(error.error_code(), 21); + assert_eq!(error.description(), "A duplicate transaction was detected."); + assert!(!error.is_retriable()); + + let error = MinaMeshError::TransactionSubmitBadNonce; + assert_eq!(error.error_code(), 22); + assert_eq!(error.description(), "The nonce is invalid."); + assert!(!error.is_retriable()); + + let error = MinaMeshError::TransactionSubmitFeeSmall; + assert_eq!(error.error_code(), 23); + assert_eq!(error.description(), "The transaction fee is too small."); + assert!(!error.is_retriable()); + + let error = MinaMeshError::TransactionSubmitInvalidSignature; + assert_eq!(error.error_code(), 24); + assert_eq!(error.description(), "The transaction signature is invalid."); + assert!(!error.is_retriable()); + + let error = MinaMeshError::TransactionSubmitInsufficientBalance; + assert_eq!(error.error_code(), 25); + assert_eq!(error.description(), "The account has insufficient balance."); + assert!(!error.is_retriable()); + + let error = MinaMeshError::TransactionSubmitExpired; + assert_eq!(error.error_code(), 26); + assert_eq!(error.description(), "The transaction has expired."); + assert!(!error.is_retriable()); +} + +#[test] +fn test_conversion_from_sqlx_error() { + let sqlx_error = sqlx::Error::RowNotFound; + let error: MinaMeshError = sqlx_error.into(); + assert!(matches!(error, MinaMeshError::Sql(_))); +} + +#[test] +fn test_conversion_from_parse_int_error() { + let parse_error: Result = "abc".parse(); + if let Err(err) = parse_error { + let error: MinaMeshError = err.into(); + assert!(matches!(error, MinaMeshError::Exception(_))); + } +} + +#[tokio::test] +async fn test_conversion_from_cynic_reqwest_error() -> Result<(), MinaMeshError> { + dotenv::dotenv().ok(); + let res = MinaMeshConfig { + proxy_url: "http://wrong-graphql".to_string(), + archive_database_url: env::var("MINAMESH_ARCHIVE_DATABASE_URL").unwrap(), + max_db_pool_size: 10, + db_pool_idle_timeout: 1, + genesis_block_identifier_height: 1, + genesis_block_identifier_state_hash: "test".to_string(), + use_search_tx_optimizations: false, + } + .to_mina_mesh() + .await? + .network_list() + .await; + // Assert that the error matches MinaMeshError::GraphqlMinaQuery + assert!(matches!(res, Err(MinaMeshError::GraphqlMinaQuery(_)))); + Ok(()) +} + +#[tokio::test] +async fn test_graphql_uri_not_set_error() -> Result<(), MinaMeshError> { + dotenv::dotenv().ok(); + let res = MinaMeshConfig { + proxy_url: "".to_string(), + archive_database_url: env::var("MINAMESH_ARCHIVE_DATABASE_URL").unwrap(), + max_db_pool_size: 10, + db_pool_idle_timeout: 1, + genesis_block_identifier_height: 1, + genesis_block_identifier_state_hash: "test".to_string(), + use_search_tx_optimizations: false, + } + .to_mina_mesh() + .await; + + assert!(matches!(res, Err(MinaMeshError::GraphqlUriNotSet))); + Ok(()) +} From 1c3012c1c45e0eb6d9f4b8759c274549012e24a5 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Tue, 19 Nov 2024 14:20:44 +0100 Subject: [PATCH 5/9] test account not found --- tests/account_balance.rs | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/tests/account_balance.rs b/tests/account_balance.rs index b7c9990..3ebf34d 100644 --- a/tests/account_balance.rs +++ b/tests/account_balance.rs @@ -5,7 +5,7 @@ use mina_mesh::{ models::{ AccountBalanceRequest, AccountBalanceResponse, AccountIdentifier, NetworkIdentifier, PartialBlockIdentifier, }, - MinaMeshConfig, + MinaMeshConfig, MinaMeshError, }; #[tokio::test] @@ -37,3 +37,28 @@ async fn responses() -> Result<()> { assert_debug_snapshot!(results); Ok(()) } + +#[tokio::test] +async fn account_not_found_error() -> Result<()> { + let mina_mesh = MinaMeshConfig::from_env().to_mina_mesh().await?; + let response = mina_mesh + .account_balance(AccountBalanceRequest { + account_identifier: Box::new(AccountIdentifier { + //cspell:disable-next-line + address: "B62qp3LaAUKQ76DdFYaQ7bj46HDTgpCaFpwhDqbjNJUC79Rf6x8CxV3".into(), + sub_account: None, + metadata: None, + }), + block_identifier: None, + currencies: None, + network_identifier: Box::new(NetworkIdentifier { + blockchain: "mina".into(), + network: "testnet".into(), + sub_network_identifier: None, + }), + }) + .await; + assert!(matches!(response, Err(MinaMeshError::AccountNotFound(_)))); + + Ok(()) +} From fbf3ce5b2d43dd3baf87287111fe5e8bc1a10f57 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Tue, 19 Nov 2024 15:05:05 +0100 Subject: [PATCH 6/9] test error properties --- src/error.rs | 3 + tests/error.rs | 238 ++++++++++++++++++++++--------------------------- 2 files changed, 108 insertions(+), 133 deletions(-) diff --git a/src/error.rs b/src/error.rs index 1e0dcbc..5592eaf 100644 --- a/src/error.rs +++ b/src/error.rs @@ -172,6 +172,9 @@ impl MinaMeshError { "error": format!("You attempted to lookup {}, but we couldn't find it in the ledger.", account), "account": account, }), + MinaMeshError::Exception(msg) => json!({ + "error": msg, + }), _ => json!(""), } } diff --git a/tests/error.rs b/tests/error.rs index f51d840..2eb41a4 100644 --- a/tests/error.rs +++ b/tests/error.rs @@ -1,140 +1,104 @@ -use std::env; +use std::{env, usize::MAX}; -// use axum::http::StatusCode; +use axum::{body::to_bytes, http::StatusCode, response::IntoResponse}; use mina_mesh::{MinaMeshConfig, MinaMeshError}; -// use serde_json::json; -#[test] -fn test_error_codes_and_descriptions() { - let error = MinaMeshError::Sql("SQL syntax error".to_string()); - assert_eq!(error.error_code(), 1); - assert_eq!(error.description(), "An SQL error occurred."); - assert!(!error.is_retriable()); - - let error = MinaMeshError::JsonParse(Some("Missing field".to_string())); - assert_eq!(error.error_code(), 2); - assert_eq!(error.description(), "We encountered an error while parsing JSON."); - assert!(!error.is_retriable()); - - let error = MinaMeshError::GraphqlMinaQuery("Timeout".to_string()); - assert_eq!(error.error_code(), 3); - assert_eq!(error.description(), "The GraphQL query failed."); - assert!(error.is_retriable()); - - let error = MinaMeshError::NetworkDne("blockchain".to_string(), "network".to_string()); - assert_eq!(error.error_code(), 4); - assert_eq!(error.description(), "The specified network does not exist."); - assert!(!error.is_retriable()); - - let error = MinaMeshError::ChainInfoMissing; - assert_eq!(error.error_code(), 5); - assert_eq!(error.description(), "Chain info is missing."); - assert!(error.is_retriable()); - - let error = MinaMeshError::AccountNotFound("Account ID".to_string()); - assert_eq!(error.error_code(), 6); - assert_eq!(error.description(), "The specified account could not be found."); - assert!(error.is_retriable()); - - let error = MinaMeshError::InvariantViolation; - assert_eq!(error.error_code(), 7); - assert_eq!(error.description(), "An internal invariant was violated."); - assert!(!error.is_retriable()); - - let error = MinaMeshError::TransactionNotFound("Transaction ID".to_string()); - assert_eq!(error.error_code(), 8); - assert_eq!(error.description(), "The specified transaction could not be found."); - assert!(error.is_retriable()); - - let error = MinaMeshError::BlockMissing("Block ID".to_string()); - assert_eq!(error.error_code(), 9); - assert_eq!(error.description(), "The specified block could not be found."); - assert!(error.is_retriable()); - - let error = MinaMeshError::MalformedPublicKey; - assert_eq!(error.error_code(), 10); - assert_eq!(error.description(), "The provided public key is malformed."); - assert!(!error.is_retriable()); - - let error = MinaMeshError::OperationsNotValid(vec![]); - assert_eq!(error.error_code(), 11); - assert_eq!(error.description(), "The provided operations are not valid."); - assert!(!error.is_retriable()); - - let error = MinaMeshError::UnsupportedOperationForConstruction; - assert_eq!(error.error_code(), 12); - assert_eq!(error.description(), "The operation is not supported for transaction construction."); - assert!(!error.is_retriable()); - - let error = MinaMeshError::SignatureMissing; - assert_eq!(error.error_code(), 13); - assert_eq!(error.description(), "A signature is missing."); - assert!(!error.is_retriable()); - - let error = MinaMeshError::PublicKeyFormatNotValid; - assert_eq!(error.error_code(), 14); - assert_eq!(error.description(), "The public key format is not valid."); - assert!(!error.is_retriable()); - - let error = MinaMeshError::NoOptionsProvided; - assert_eq!(error.error_code(), 15); - assert_eq!(error.description(), "No options were provided."); - assert!(!error.is_retriable()); - - let error = MinaMeshError::Exception("Unexpected error".to_string()); - assert_eq!(error.error_code(), 16); - assert_eq!(error.description(), "An internal exception occurred."); - assert!(!error.is_retriable()); - - let error = MinaMeshError::SignatureInvalid; - assert_eq!(error.error_code(), 17); - assert_eq!(error.description(), "The signature is invalid."); - assert!(!error.is_retriable()); - - let error = MinaMeshError::MemoInvalid; - assert_eq!(error.error_code(), 18); - assert_eq!(error.description(), "The memo is invalid."); - assert!(!error.is_retriable()); - - let error = MinaMeshError::GraphqlUriNotSet; - assert_eq!(error.error_code(), 19); - assert_eq!(error.description(), "No GraphQL URI has been set."); - assert!(!error.is_retriable()); - - let error = MinaMeshError::TransactionSubmitNoSender; - assert_eq!(error.error_code(), 20); - assert_eq!(error.description(), "No sender was found in the ledger."); - assert!(error.is_retriable()); - - let error = MinaMeshError::TransactionSubmitDuplicate; - assert_eq!(error.error_code(), 21); - assert_eq!(error.description(), "A duplicate transaction was detected."); - assert!(!error.is_retriable()); - - let error = MinaMeshError::TransactionSubmitBadNonce; - assert_eq!(error.error_code(), 22); - assert_eq!(error.description(), "The nonce is invalid."); - assert!(!error.is_retriable()); - - let error = MinaMeshError::TransactionSubmitFeeSmall; - assert_eq!(error.error_code(), 23); - assert_eq!(error.description(), "The transaction fee is too small."); - assert!(!error.is_retriable()); - - let error = MinaMeshError::TransactionSubmitInvalidSignature; - assert_eq!(error.error_code(), 24); - assert_eq!(error.description(), "The transaction signature is invalid."); - assert!(!error.is_retriable()); - - let error = MinaMeshError::TransactionSubmitInsufficientBalance; - assert_eq!(error.error_code(), 25); - assert_eq!(error.description(), "The account has insufficient balance."); - assert!(!error.is_retriable()); +async fn assert_error_properties( + error: MinaMeshError, + expected_code: u8, + expected_description: &str, + expected_retriable: bool, + expected_status: StatusCode, +) { + assert_eq!(error.error_code(), expected_code); + assert_eq!(error.description(), expected_description); + assert_eq!(error.is_retriable(), expected_retriable); + + let message = error.to_string(); + let response = error.into_response(); + assert_eq!(response.status(), expected_status); + + let body = to_bytes(response.into_body(), MAX).await.unwrap(); + let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); + + assert_eq!(json["code"], expected_code); + assert_eq!(json["message"], message); + assert_eq!(json["description"], expected_description); + assert_eq!(json["retriable"], expected_retriable); +} - let error = MinaMeshError::TransactionSubmitExpired; - assert_eq!(error.error_code(), 26); - assert_eq!(error.description(), "The transaction has expired."); - assert!(!error.is_retriable()); +#[tokio::test] +async fn test_error_properties() { + use MinaMeshError::*; + + let cases = vec![ + (Sql("SQL syntax error".to_string()), 1, "An SQL error occurred.", false, StatusCode::INTERNAL_SERVER_ERROR), + ( + JsonParse(Some("Missing field".to_string())), + 2, + "We encountered an error while parsing JSON.", + false, + StatusCode::BAD_REQUEST, + ), + (GraphqlMinaQuery("Timeout".to_string()), 3, "The GraphQL query failed.", true, StatusCode::BAD_GATEWAY), + ( + NetworkDne("blockchain".to_string(), "network".to_string()), + 4, + "The specified network does not exist.", + false, + StatusCode::NOT_FOUND, + ), + (ChainInfoMissing, 5, "Chain info is missing.", true, StatusCode::INTERNAL_SERVER_ERROR), + ( + AccountNotFound("Account ID".to_string()), + 6, + "The specified account could not be found.", + true, + StatusCode::NOT_FOUND, + ), + (InvariantViolation, 7, "An internal invariant was violated.", false, StatusCode::INTERNAL_SERVER_ERROR), + ( + TransactionNotFound("Transaction ID".to_string()), + 8, + "The specified transaction could not be found.", + true, + StatusCode::NOT_FOUND, + ), + (BlockMissing("Block ID".to_string()), 9, "The specified block could not be found.", true, StatusCode::NOT_FOUND), + (MalformedPublicKey, 10, "The provided public key is malformed.", false, StatusCode::BAD_REQUEST), + (OperationsNotValid(vec![]), 11, "The provided operations are not valid.", false, StatusCode::BAD_REQUEST), + ( + UnsupportedOperationForConstruction, + 12, + "The operation is not supported for transaction construction.", + false, + StatusCode::BAD_REQUEST, + ), + (SignatureMissing, 13, "A signature is missing.", false, StatusCode::BAD_REQUEST), + (PublicKeyFormatNotValid, 14, "The public key format is not valid.", false, StatusCode::BAD_REQUEST), + (NoOptionsProvided, 15, "No options were provided.", false, StatusCode::BAD_REQUEST), + ( + Exception("Unexpected error".to_string()), + 16, + "An internal exception occurred.", + false, + StatusCode::INTERNAL_SERVER_ERROR, + ), + (SignatureInvalid, 17, "The signature is invalid.", false, StatusCode::BAD_REQUEST), + (MemoInvalid, 18, "The memo is invalid.", false, StatusCode::BAD_REQUEST), + (GraphqlUriNotSet, 19, "No GraphQL URI has been set.", false, StatusCode::INTERNAL_SERVER_ERROR), + (TransactionSubmitNoSender, 20, "No sender was found in the ledger.", true, StatusCode::BAD_REQUEST), + (TransactionSubmitDuplicate, 21, "A duplicate transaction was detected.", false, StatusCode::CONFLICT), + (TransactionSubmitBadNonce, 22, "The nonce is invalid.", false, StatusCode::BAD_REQUEST), + (TransactionSubmitFeeSmall, 23, "The transaction fee is too small.", false, StatusCode::BAD_REQUEST), + (TransactionSubmitInvalidSignature, 24, "The transaction signature is invalid.", false, StatusCode::BAD_REQUEST), + (TransactionSubmitInsufficientBalance, 25, "The account has insufficient balance.", false, StatusCode::BAD_REQUEST), + (TransactionSubmitExpired, 26, "The transaction has expired.", false, StatusCode::BAD_REQUEST), + ]; + + for (error, code, description, retriable, status) in cases { + assert_error_properties(error, code, description, retriable, status).await; + } } #[test] @@ -174,6 +138,14 @@ async fn test_conversion_from_cynic_reqwest_error() -> Result<(), MinaMeshError> Ok(()) } +#[test] +fn test_conversion_from_anyhow_error() { + let anyhow_error = anyhow::Error::msg("Unexpected issue"); + let error: MinaMeshError = anyhow_error.into(); + + assert!(matches!(error, MinaMeshError::Exception(_))); +} + #[tokio::test] async fn test_graphql_uri_not_set_error() -> Result<(), MinaMeshError> { dotenv::dotenv().ok(); From 147dcf665da536aee035df079be5189940d5caf3 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Tue, 19 Nov 2024 15:14:25 +0100 Subject: [PATCH 7/9] clippy fixup --- tests/error.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/error.rs b/tests/error.rs index 2eb41a4..0a20ef6 100644 --- a/tests/error.rs +++ b/tests/error.rs @@ -1,4 +1,4 @@ -use std::{env, usize::MAX}; +use std::env; use axum::{body::to_bytes, http::StatusCode, response::IntoResponse}; use mina_mesh::{MinaMeshConfig, MinaMeshError}; @@ -18,7 +18,7 @@ async fn assert_error_properties( let response = error.into_response(); assert_eq!(response.status(), expected_status); - let body = to_bytes(response.into_body(), MAX).await.unwrap(); + let body = to_bytes(response.into_body(), usize::MAX).await.unwrap(); let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); assert_eq!(json["code"], expected_code); From f90533fcc32a0c070b6bbbb04b4c253935db88e1 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Wed, 20 Nov 2024 12:48:20 +0100 Subject: [PATCH 8/9] use actual errors in network/options --- src/api/account_balance.rs | 1 - src/api/network_options.rs | 220 +------------------------------------ src/error.rs | 45 +++++++- tests/error.rs | 2 +- 4 files changed, 48 insertions(+), 220 deletions(-) diff --git a/src/api/account_balance.rs b/src/api/account_balance.rs index c8b885e..c5e392f 100644 --- a/src/api/account_balance.rs +++ b/src/api/account_balance.rs @@ -60,7 +60,6 @@ impl MinaMesh { }])) } Some(account_balance_info) => { - println!("B"); let last_relevant_command_balance = account_balance_info.balance.parse::()?; let timing_info = sqlx::query_file!("sql/queries/timing_info.sql", account_balance_info.timing_id) .fetch_optional(&self.pg_pool) diff --git a/src/api/network_options.rs b/src/api/network_options.rs index ed8c026..9199189 100644 --- a/src/api/network_options.rs +++ b/src/api/network_options.rs @@ -8,6 +8,8 @@ use crate::{MinaMesh, MinaMeshError}; /// https://github.com/MinaProtocol/mina/blob/985eda49bdfabc046ef9001d3c406e688bc7ec45/src/app/rosetta/lib/network.ml#L444 impl MinaMesh { pub async fn network_options(&self) -> Result { + let errors: Vec = MinaMeshError::all_errors().into_iter().map(Error::from).collect(); + Ok(NetworkOptionsResponse::new(Version::new("1.4.9".to_string(), "1.0.0".to_string()), Allow { operation_statuses: vec![ OperationStatus::new("Success".to_string(), true), @@ -32,223 +34,7 @@ impl MinaMesh { .into_iter() .map(|s| s.to_string()) .collect(), - errors: vec![ - Error { - code: 1, - message: "SQL failure".to_string(), - description: Some("We encountered a SQL failure.".to_string()), - retriable: false, - details: None, - }, - Error { - code: 2, - message: "JSON parse error".to_string(), - description: Some("We encountered an error while parsing JSON.".to_string()), - retriable: false, - details: None, - }, - Error { - code: 3, - message: "GraphQL query failed".to_string(), - description: Some("The GraphQL query failed.".to_string()), - retriable: true, - details: None, - }, - Error { - code: 4, - message: "Network doesn't exist".to_string(), - description: Some("The network doesn't exist.".to_string()), - retriable: false, - details: None, - }, - Error { - code: 5, - message: "Chain info missing".to_string(), - description: Some("Some chain info is missing.".to_string()), - retriable: true, - details: None, - }, - Error { - code: 6, - message: "Account not found".to_string(), - description: Some("That account could not be found.".to_string()), - retriable: true, - details: None, - }, - Error { - code: 7, - message: "Internal invariant violation (you found a bug)".to_string(), - description: Some("One of our internal invariants was violated. (That means you found a bug!)".to_string()), - retriable: false, - details: None, - }, - Error { - code: 8, - message: "Transaction not found".to_string(), - description: Some("That transaction could not be found.".to_string()), - retriable: true, - details: None, - }, - Error { - code: 9, - message: "Block not found".to_string(), - description: Some( - "We couldn't find the block in the archive node, specified by . Ask a friend for the missing data." - .to_string(), - ), - retriable: true, - details: None, - }, - Error { - code: 10, - message: "Malformed public key".to_string(), - description: Some("The public key you provided was malformed.".to_string()), - retriable: false, - details: None, - }, - Error { - code: 11, - message: "Cannot convert operations to valid transaction".to_string(), - description: Some("We could not convert those operations to a valid transaction.".to_string()), - retriable: false, - details: None, - }, - Error { - code: 12, - message: "Unsupported operation for construction".to_string(), - description: Some("An operation you provided isn't supported for construction.".to_string()), - retriable: false, - details: None, - }, - Error { - code: 13, - message: "Signature missing".to_string(), - description: Some("Your request is missing a signature.".to_string()), - retriable: false, - details: None, - }, - Error { - code: 14, - message: "Invalid public key format".to_string(), - description: Some("The public key you provided had an invalid format.".to_string()), - retriable: false, - details: None, - }, - Error { - code: 15, - message: "No options provided".to_string(), - description: Some("Your request is missing options.".to_string()), - retriable: false, - details: None, - }, - Error { - code: 16, - message: "Exception".to_string(), - description: Some( - "We encountered an internal exception while processing your request. (That means you found a bug!)" - .to_string(), - ), - retriable: false, - details: None, - }, - Error { - code: 17, - message: "Invalid signature".to_string(), - description: Some("Your request has an invalid signature.".to_string()), - retriable: false, - details: None, - }, - Error { - code: 18, - message: "Invalid memo".to_string(), - description: Some("Your request has an invalid memo.".to_string()), - retriable: false, - details: None, - }, - Error { - code: 19, - message: "No GraphQL URI set".to_string(), - description: Some( - "This Rosetta instance is running without a GraphQL URI set but this request requires one.".to_string(), - ), - retriable: false, - details: None, - }, - Error { - code: 20, - message: "Can't send transaction: No sender found in ledger".to_string(), - description: Some( - #[allow(clippy::useless_vec)] - vec![ - "This could occur because the node isn't fully synced", - "or the account doesn't actually exist in the ledger yet.", - ] - .join(" "), - ), - retriable: true, - details: None, - }, - Error { - code: 21, - message: "Can't send transaction: A duplicate is detected".to_string(), - description: Some( - #[allow(clippy::useless_vec)] - vec![ - "This could occur if you've already sent this transaction.", - "Please report a bug if you are confident you didn't already send this exact transaction.", - ] - .join(" "), - ), - retriable: false, - details: None, - }, - Error { - code: 22, - message: "Can't send transaction: Nonce invalid".to_string(), - description: Some( - #[allow(clippy::useless_vec)] - vec![ - "You must use the current nonce in your account in the ledger", - "or one that is inferred based on pending transactions in the transaction pool.", - ] - .join(" "), - ), - retriable: false, - details: None, - }, - Error { - code: 23, - message: "Can't send transaction: Fee too small".to_string(), - description: Some( - "The minimum fee on transactions is 0.001 . Please increase your fee to at least this amount.".to_string(), - ), - retriable: false, - details: None, - }, - Error { - code: 24, - message: "Can't send transaction: Invalid signature".to_string(), - description: Some("An invalid signature is attached to this transaction".to_string()), - retriable: false, - details: None, - }, - Error { - code: 25, - message: "Can't send transaction: Insufficient balance".to_string(), - description: Some( - "This account do not have sufficient balance perform the requested transaction.".to_string(), - ), - retriable: false, - details: None, - }, - Error { - code: 26, - message: "Can't send transaction: Expired".to_string(), - description: Some("This transaction is expired. Please try again with a larger valid_until.".to_string()), - retriable: false, - details: None, - }, - ], + errors, historical_balance_lookup: true, timestamp_start_index: None, call_methods: vec![], diff --git a/src/error.rs b/src/error.rs index 5592eaf..6171477 100644 --- a/src/error.rs +++ b/src/error.rs @@ -108,8 +108,39 @@ pub enum PartialReason { } impl MinaMeshError { + pub fn all_errors() -> Vec { + vec![ + MinaMeshError::Sql("SQL syntax error".to_string()), + MinaMeshError::JsonParse(Some("Missing field".to_string())), + MinaMeshError::GraphqlMinaQuery("Timeout".to_string()), + MinaMeshError::NetworkDne("blockchain".to_string(), "network".to_string()), + MinaMeshError::ChainInfoMissing, + MinaMeshError::AccountNotFound("Account ID".to_string()), + MinaMeshError::InvariantViolation, + MinaMeshError::TransactionNotFound("Transaction ID".to_string()), + MinaMeshError::BlockMissing("Block ID".to_string()), + MinaMeshError::MalformedPublicKey, + MinaMeshError::OperationsNotValid(vec![]), + MinaMeshError::UnsupportedOperationForConstruction, + MinaMeshError::SignatureMissing, + MinaMeshError::PublicKeyFormatNotValid, + MinaMeshError::NoOptionsProvided, + MinaMeshError::Exception("Unexpected error".to_string()), + MinaMeshError::SignatureInvalid, + MinaMeshError::MemoInvalid, + MinaMeshError::GraphqlUriNotSet, + MinaMeshError::TransactionSubmitNoSender, + MinaMeshError::TransactionSubmitDuplicate, + MinaMeshError::TransactionSubmitBadNonce, + MinaMeshError::TransactionSubmitFeeSmall, + MinaMeshError::TransactionSubmitInvalidSignature, + MinaMeshError::TransactionSubmitInsufficientBalance, + MinaMeshError::TransactionSubmitExpired, + ] + } + /// Returns the error code for the error. - pub fn error_code(&self) -> u8 { + pub fn error_code(&self) -> i32 { match self { MinaMeshError::Sql(_) => 1, MinaMeshError::JsonParse(_) => 2, @@ -313,3 +344,15 @@ impl From for MinaMeshError { MinaMeshError::JsonParse(Some(err.body_text())) } } + +impl From for coinbase_mesh::models::Error { + fn from(error: MinaMeshError) -> Self { + coinbase_mesh::models::Error { + code: error.error_code(), + message: error.to_string(), + description: Some(error.description()), + retriable: error.is_retriable(), + details: Some(error.details()), + } + } +} diff --git a/tests/error.rs b/tests/error.rs index 0a20ef6..37329b0 100644 --- a/tests/error.rs +++ b/tests/error.rs @@ -5,7 +5,7 @@ use mina_mesh::{MinaMeshConfig, MinaMeshError}; async fn assert_error_properties( error: MinaMeshError, - expected_code: u8, + expected_code: i32, expected_description: &str, expected_retriable: bool, expected_status: StatusCode, From 57a634d4d9047348e8ca843e7316180f96e8f401 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Wed, 20 Nov 2024 12:48:29 +0100 Subject: [PATCH 9/9] test network options --- tests/network_options.rs | 10 + .../network_options__network_options.snap | 349 ++++++++++++++++++ 2 files changed, 359 insertions(+) create mode 100644 tests/network_options.rs create mode 100644 tests/snapshots/network_options__network_options.snap diff --git a/tests/network_options.rs b/tests/network_options.rs new file mode 100644 index 0000000..383061b --- /dev/null +++ b/tests/network_options.rs @@ -0,0 +1,10 @@ +use anyhow::Result; +use insta::assert_debug_snapshot; +use mina_mesh::MinaMeshConfig; + +#[tokio::test] +async fn test_network_options() -> Result<()> { + let response = MinaMeshConfig::from_env().to_mina_mesh().await?.network_options().await?; + assert_debug_snapshot!(&response.allow); + Ok(()) +} diff --git a/tests/snapshots/network_options__network_options.snap b/tests/snapshots/network_options__network_options.snap new file mode 100644 index 0000000..351b22f --- /dev/null +++ b/tests/snapshots/network_options__network_options.snap @@ -0,0 +1,349 @@ +--- +source: tests/network_options.rs +expression: "&response.allow" +--- +Allow { + operation_statuses: [ + OperationStatus { + status: "Success", + successful: true, + }, + OperationStatus { + status: "Failed", + successful: false, + }, + ], + operation_types: [ + "fee_payer_dec", + "fee_receiver_inc", + "coinbase_inc", + "account_creation_fee_via_payment", + "account_creation_fee_via_fee_payer", + "account_creation_fee_via_fee_receiver", + "payment_source_dec", + "payment_receiver_inc", + "fee_payment", + "delegate_change", + "create_token", + "mint_tokens", + "zkapp_fee_payer_dec", + "zkapp_balance_update", + ], + errors: [ + Error { + code: 1, + message: "SQL failure: SQL syntax error", + description: Some( + "An SQL error occurred.", + ), + retriable: false, + details: Some( + Object { + "error": String("SQL syntax error"), + "extra": String("Internal SQL query failed"), + }, + ), + }, + Error { + code: 2, + message: "JSON parse error", + description: Some( + "We encountered an error while parsing JSON.", + ), + retriable: false, + details: Some( + Object { + "error": String("Missing field"), + "extra": String("Failed to parse JSON body"), + }, + ), + }, + Error { + code: 3, + message: "GraphQL query failed: Timeout", + description: Some( + "The GraphQL query failed.", + ), + retriable: true, + details: Some( + Object { + "error": String("Timeout"), + "extra": String("Internal POST to Mina Daemon failed"), + }, + ), + }, + Error { + code: 4, + message: "Network doesn't exist", + description: Some( + "The specified network does not exist.", + ), + retriable: false, + details: Some( + String(""), + ), + }, + Error { + code: 5, + message: "Chain info missing", + description: Some( + "Chain info is missing.", + ), + retriable: true, + details: Some( + String(""), + ), + }, + Error { + code: 6, + message: "Account not found: Account ID", + description: Some( + "The specified account could not be found.", + ), + retriable: true, + details: Some( + Object { + "account": String("Account ID"), + "error": String("You attempted to lookup Account ID, but we couldn't find it in the ledger."), + }, + ), + }, + Error { + code: 7, + message: "Internal invariant violation (you found a bug)", + description: Some( + "An internal invariant was violated.", + ), + retriable: false, + details: Some( + String(""), + ), + }, + Error { + code: 8, + message: "Transaction not found", + description: Some( + "The specified transaction could not be found.", + ), + retriable: true, + details: Some( + String(""), + ), + }, + Error { + code: 9, + message: "Block not found", + description: Some( + "The specified block could not be found.", + ), + retriable: true, + details: Some( + String(""), + ), + }, + Error { + code: 10, + message: "Malformed public key", + description: Some( + "The provided public key is malformed.", + ), + retriable: false, + details: Some( + String(""), + ), + }, + Error { + code: 11, + message: "Cannot convert operations to valid transaction", + description: Some( + "The provided operations are not valid.", + ), + retriable: false, + details: Some( + String(""), + ), + }, + Error { + code: 12, + message: "Unsupported operation for construction", + description: Some( + "The operation is not supported for transaction construction.", + ), + retriable: false, + details: Some( + String(""), + ), + }, + Error { + code: 13, + message: "Signature missing", + description: Some( + "A signature is missing.", + ), + retriable: false, + details: Some( + String(""), + ), + }, + Error { + code: 14, + message: "Invalid public key format", + description: Some( + "The public key format is not valid.", + ), + retriable: false, + details: Some( + String(""), + ), + }, + Error { + code: 15, + message: "No options provided", + description: Some( + "No options were provided.", + ), + retriable: false, + details: Some( + String(""), + ), + }, + Error { + code: 16, + message: "Exception Unexpected error", + description: Some( + "An internal exception occurred.", + ), + retriable: false, + details: Some( + Object { + "error": String("Unexpected error"), + }, + ), + }, + Error { + code: 17, + message: "Invalid signature", + description: Some( + "The signature is invalid.", + ), + retriable: false, + details: Some( + String(""), + ), + }, + Error { + code: 18, + message: "Invalid memo", + description: Some( + "The memo is invalid.", + ), + retriable: false, + details: Some( + String(""), + ), + }, + Error { + code: 19, + message: "No GraphQL URI set", + description: Some( + "No GraphQL URI has been set.", + ), + retriable: false, + details: Some( + String(""), + ), + }, + Error { + code: 20, + message: "Can't send transaction: No sender found in ledger", + description: Some( + "No sender was found in the ledger.", + ), + retriable: true, + details: Some( + String(""), + ), + }, + Error { + code: 21, + message: "Can't send transaction: A duplicate is detected", + description: Some( + "A duplicate transaction was detected.", + ), + retriable: false, + details: Some( + String(""), + ), + }, + Error { + code: 22, + message: "Can't send transaction: Nonce invalid", + description: Some( + "The nonce is invalid.", + ), + retriable: false, + details: Some( + String(""), + ), + }, + Error { + code: 23, + message: "Can't send transaction: Fee too small", + description: Some( + "The transaction fee is too small.", + ), + retriable: false, + details: Some( + String(""), + ), + }, + Error { + code: 24, + message: "Can't send transaction: Invalid signature", + description: Some( + "The transaction signature is invalid.", + ), + retriable: false, + details: Some( + String(""), + ), + }, + Error { + code: 25, + message: "Can't send transaction: Insufficient balance", + description: Some( + "The account has insufficient balance.", + ), + retriable: false, + details: Some( + String(""), + ), + }, + Error { + code: 26, + message: "Can't send transaction: Expired", + description: Some( + "The transaction has expired.", + ), + retriable: false, + details: Some( + String(""), + ), + }, + ], + historical_balance_lookup: true, + timestamp_start_index: None, + call_methods: [], + balance_exemptions: [], + mempool_coins: false, + block_hash_case: Some( + Some( + CaseSensitive, + ), + ), + transaction_hash_case: Some( + Some( + CaseSensitive, + ), + ), +}