Skip to content

Commit

Permalink
More comparison legacy testing (mempool, network) (#75)
Browse files Browse the repository at this point in the history
  • Loading branch information
piotr-iohk authored Nov 29, 2024
1 parent 771592e commit 5431840
Show file tree
Hide file tree
Showing 13 changed files with 268 additions and 65 deletions.
21 changes: 21 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ reqwest = { version = "0.12.5", features = ["json", "blocking"] }
serde = { version = "1.0.204", features = ["derive"] }
serde_json = { version = "1.0.121" }
sqlx = { version = "0.8.0", features = ["runtime-tokio", "postgres", "json"] }
strum = "0.26.3"
strum_macros = "0.26.4"
thiserror = "1.0.63"
tokio = { version = "1.39.2", features = ["full"] }
tower = "0.5.1"
Expand Down
7 changes: 7 additions & 0 deletions src/api/mempool_transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,14 @@ impl MinaMesh {
hashes: Some(vec![request.transaction_identifier.hash.as_str()]),
}))
.await?;

// Check if the transaction is absent
if pooled_user_commands.is_empty() {
return Err(MinaMeshError::TransactionNotFound(request.transaction_identifier.hash));
}

let operations = pooled_user_commands.into_iter().map(Into::into).collect();

Ok(MempoolTransactionResponse {
metadata: None,
transaction: Box::new(Transaction {
Expand Down
22 changes: 2 additions & 20 deletions src/api/network_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

use coinbase_mesh::models::{Allow, Case, Error, NetworkOptionsResponse, NetworkRequest, OperationStatus, Version};

use crate::{MinaMesh, MinaMeshError};
use crate::{operation_types, MinaMesh, MinaMeshError};

/// https://github.com/MinaProtocol/mina/blob/985eda49bdfabc046ef9001d3c406e688bc7ec45/src/app/rosetta/lib/network.ml#L444
impl MinaMesh {
Expand All @@ -16,25 +16,7 @@ impl MinaMesh {
OperationStatus::new("Success".to_string(), true),
OperationStatus::new("Failed".to_string(), false),
],
operation_types: vec![
"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",
]
.into_iter()
.map(|s| s.to_string())
.collect(),
operation_types: operation_types(),
errors,
historical_balance_lookup: true,
timestamp_start_index: None,
Expand Down
44 changes: 32 additions & 12 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ pub enum MinaMeshError {
#[error("Internal invariant violation (you found a bug)")]
InvariantViolation,

#[error("Transaction not found")]
#[error("Transaction not found: {0}")]
TransactionNotFound(String),

#[error("Block not found")]
Expand Down Expand Up @@ -209,6 +209,15 @@ impl MinaMeshError {
MinaMeshError::NetworkDne(expected, actual) => json!({
"error": format!("You are requesting the status for the network {}, but you are connected to the network {}", expected, actual),
}),
MinaMeshError::TransactionNotFound(tx) => json!({
"error": format!(
"You attempted to lookup transaction {}, but it is missing from the mempool. {} {}",
tx,
"This may be due to its inclusion in a block -- try looking for this transaction in a recent block.",
"It also could be due to the transaction being evicted from the mempool."
),
"transaction": tx,
}),
_ => json!(""),
}
}
Expand All @@ -227,7 +236,7 @@ impl MinaMeshError {
/// Returns a human-readable description of the error.
pub fn description(&self) -> String {
match self {
MinaMeshError::Sql(_) => "An SQL error occurred.".to_string(),
MinaMeshError::Sql(_) => "We encountered a SQL failure.".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(),
Expand All @@ -237,24 +246,35 @@ impl MinaMeshError {
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::OperationsNotValid(_) => {
"We could not convert those operations to a valid transaction.".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::SignatureMissing => "Your request is missing a signature.".to_string(),
MinaMeshError::PublicKeyFormatNotValid => "The public key you provided had an invalid format.".to_string(),
MinaMeshError::NoOptionsProvided => "Your request is missing options.".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::SignatureInvalid => "Your request has an invalid signature.".to_string(),
MinaMeshError::MemoInvalid => "Your request has an invalid memo.".to_string(),
MinaMeshError::GraphqlUriNotSet => "No GraphQL URI has been set.".to_string(),
MinaMeshError::TransactionSubmitNoSender => "No sender was found in the ledger.".to_string(),
MinaMeshError::TransactionSubmitNoSender => {
"This could occur because the node isn't fully synced or the account doesn't actually exist in the ledger yet."
.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(),
MinaMeshError::TransactionSubmitInvalidSignature => {
"An invalid signature is attached to this transaction.".to_string()
}
MinaMeshError::TransactionSubmitInsufficientBalance => {
"This account do not have sufficient balance perform the requested transaction.".to_string()
}
MinaMeshError::TransactionSubmitExpired => {
"This transaction is expired. Please try again with a larger valid_until.".to_string()
}
}
}
}
Expand Down
25 changes: 25 additions & 0 deletions src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,31 @@ impl ResponseComparisonContext {
Ok(())
}

pub async fn assert_responses_contain(
&self,
subpath: &str,
maybe_body_bytes: Option<Vec<u8>>,
expected_fragment: &str,
) -> Result<()> {
let body_bytes = maybe_body_bytes.clone().unwrap_or_default();
let (a, b) =
tokio::try_join!(self.mina_mesh_req(subpath, body_bytes.clone()), self.legacy_req(subpath, body_bytes))?;

// Check if the expected fragment is present in both responses
let a_contains = a.contains(expected_fragment);
let b_contains = b.contains(expected_fragment);

assert!(
a_contains && b_contains,
"Mismatch for {subpath}; expected fragment `{}` not found in one or both responses; mina_mesh: {}, rosetta: {}",
expected_fragment,
a,
b
);

Ok(())
}

async fn mina_mesh_req(&self, subpath: &str, body_bytes: Vec<u8>) -> Result<String> {
let oneshot_req = Request::builder()
.method("POST")
Expand Down
12 changes: 8 additions & 4 deletions src/types.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use convert_case::{Case, Casing};
use derive_more::derive::Display;
use serde::Serialize;
use sqlx::{FromRow, Type};
use strum::IntoEnumIterator;
use strum_macros::EnumIter;

#[derive(Type, Debug, PartialEq, Eq, Serialize)]
#[sqlx(type_name = "chain_status_type", rename_all = "lowercase")]
Expand Down Expand Up @@ -47,24 +50,25 @@ impl From<TransactionStatus> for OperationStatus {
}
}

#[derive(Debug, Display)]
#[derive(Debug, Display, EnumIter)]
pub enum OperationType {
FeePayerDec,
FeeReceiverInc,
CoinbaseInc,
AccountCreationFeeViaPayment,
AccountCreationFeeViaFeePayer,
AccountCreationFeeViaFeeReceiver,
PaymentSourceDec,
PaymentReceiverInc,
FeePayment,
DelegateChange,
CreateToken,
MintTokens,
ZkappFeePayerDec,
ZkappBalanceUpdate,
}

pub fn operation_types() -> Vec<String> {
OperationType::iter().map(|variant| format!("{:?}", variant).to_case(Case::Snake)).collect()
}

#[derive(Type, Debug, PartialEq, Eq, Serialize, Display)]
#[sqlx(type_name = "may_use_token")]
pub enum MayUseToken {
Expand Down
49 changes: 47 additions & 2 deletions tests/compare_to_ocaml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,64 @@ use serde::Serialize;

const LEGACY_ENDPOINT: &str = "https://rosetta-devnet.minaprotocol.network";

async fn compare_responses<T: Serialize>(subpath: &str, reqs: &[T]) -> Result<()> {
async fn assert_responses_eq<T: Serialize>(subpath: &str, reqs: &[T]) -> Result<()> {
let mina_mesh = MinaMeshConfig::from_env().to_mina_mesh().await?;
let comparison_ctx = ResponseComparisonContext::new(mina_mesh, LEGACY_ENDPOINT.to_string());
let assertion_futures: Vec<_> = reqs
.iter()
.map(|r| serde_json::to_vec(r).map(|body| comparison_ctx.assert_responses_eq(subpath, Some(body))).unwrap())
.collect();

join_all(assertion_futures).await;
Ok(())
}

async fn assert_responses_contain<T: Serialize>(subpath: &str, reqs: &[T], fragment: &str) -> Result<()> {
let mina_mesh = MinaMeshConfig::from_env().to_mina_mesh().await?;
let comparison_ctx = ResponseComparisonContext::new(mina_mesh, LEGACY_ENDPOINT.to_string());
let assertion_futures: Vec<_> = reqs
.iter()
.map(|r| {
serde_json::to_vec(r).map(|body| comparison_ctx.assert_responses_contain(subpath, Some(body), fragment)).unwrap()
})
.collect();

join_all(assertion_futures).await;
Ok(())
}

#[tokio::test]
async fn search_transactions() -> Result<()> {
let (subpath, reqs) = fixtures::search_transactions();
compare_responses(subpath, &reqs).await
assert_responses_eq(subpath, &reqs).await
}

#[tokio::test]
async fn network_list() -> Result<()> {
let (subpath, reqs) = fixtures::network_list();
assert_responses_eq(subpath, &reqs).await
}

#[tokio::test]
async fn network_options() -> Result<()> {
let (subpath, reqs) = fixtures::network_options();
assert_responses_contain(subpath, &reqs, "node_version").await
}

#[tokio::test]
async fn network_status() -> Result<()> {
let (subpath, reqs) = fixtures::network_status();
assert_responses_contain(subpath, &reqs, "\"stage\": \"Synced\"").await
}

#[tokio::test]
async fn mempool() -> Result<()> {
let (subpath, reqs) = fixtures::mempool();
assert_responses_eq(subpath, &reqs).await
}

#[tokio::test]
async fn mempool_transaction() -> Result<()> {
let (subpath, reqs) = fixtures::mempool_transaction();
assert_responses_contain(subpath, &reqs, "\"message\": \"Transaction not found").await
}
52 changes: 41 additions & 11 deletions tests/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ 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),
(Sql("SQL syntax error".to_string()), 1, "We encountered a SQL failure.", false, StatusCode::INTERNAL_SERVER_ERROR),
(
JsonParse(Some("Missing field".to_string())),
2,
Expand Down Expand Up @@ -66,34 +66,64 @@ async fn test_error_properties() {
),
(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),
(
OperationsNotValid(vec![]),
11,
"We could not convert those operations to a valid transaction.",
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),
(SignatureMissing, 13, "Your request is missing a signature.", false, StatusCode::BAD_REQUEST),
(PublicKeyFormatNotValid, 14, "The public key you provided had an invalid format.", false, StatusCode::BAD_REQUEST),
(NoOptionsProvided, 15, "Your request is missing options.", 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),
(SignatureInvalid, 17, "Your request has an invalid signature.", false, StatusCode::BAD_REQUEST),
(MemoInvalid, 18, "Your request has an invalid memo.", 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),
(
TransactionSubmitNoSender,
20,
"This could occur because the node isn't fully synced or the account doesn't actually exist in the ledger yet.",
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),
(
TransactionSubmitInvalidSignature,
24,
"An invalid signature is attached to this transaction.",
false,
StatusCode::BAD_REQUEST,
),
(
TransactionSubmitInsufficientBalance,
25,
"This account do not have sufficient balance perform the requested transaction.",
false,
StatusCode::BAD_REQUEST,
),
(
TransactionSubmitExpired,
26,
"This transaction is expired. Please try again with a larger valid_until.",
false,
StatusCode::BAD_REQUEST,
),
];

for (error, code, description, retriable, status) in cases {
Expand Down
Loading

0 comments on commit 5431840

Please sign in to comment.