diff --git a/Cargo.lock b/Cargo.lock index 6fd8d94..36f45a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -55,21 +55,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "aide" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0e3b97a21e41ec5c19bfd9b4fc1f7086be104f8b988681230247ffc91cc8ed" -dependencies = [ - "cfg-if", - "indexmap 2.2.6", - "schemars", - "serde", - "serde_json", - "thiserror", - "tracing", -] - [[package]] name = "aliasable" version = "0.1.3" @@ -198,9 +183,9 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "axum" -version = "0.7.5" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" dependencies = [ "async-trait", "axum-core", @@ -225,7 +210,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper 1.0.1", "tokio", - "tower", + "tower 0.5.1", "tower-layer", "tower-service", "tracing", @@ -233,9 +218,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" dependencies = [ "async-trait", "bytes", @@ -246,7 +231,7 @@ dependencies = [ "mime", "pin-project-lite", "rustversion", - "sync_wrapper 0.1.2", + "sync_wrapper 1.0.1", "tower-layer", "tower-service", "tracing", @@ -812,12 +797,6 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" -[[package]] -name = "dyn-clone" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" - [[package]] name = "either" version = "1.13.0" @@ -842,21 +821,22 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "envy" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965" -dependencies = [ - "serde", -] - [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "erased-serde" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d" +dependencies = [ + "serde", + "typeid", +] + [[package]] name = "errno" version = "0.3.9" @@ -944,9 +924,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -959,9 +939,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -969,15 +949,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -997,15 +977,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -1014,21 +994,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -1292,7 +1272,7 @@ dependencies = [ "pin-project-lite", "socket2", "tokio", - "tower", + "tower 0.4.13", "tower-service", "tracing", ] @@ -1556,7 +1536,6 @@ dependencies = [ name = "mina_mesh" version = "0.0.0" dependencies = [ - "aide", "anyhow", "axum", "bs58", @@ -1569,10 +1548,13 @@ dependencies = [ "dashmap", "derive_more", "dotenv", - "envy", + "erased-serde", "futures", + "http", + "http-body-util", "indoc", "insta", + "mime", "paste", "pretty_assertions", "reqwest", @@ -1581,6 +1563,7 @@ dependencies = [ "sqlx", "thiserror", "tokio", + "tower 0.5.1", "tracing", "tracing-subscriber", ] @@ -2314,31 +2297,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "schemars" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" -dependencies = [ - "dyn-clone", - "indexmap 2.2.6", - "schemars_derive", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.72", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -2394,17 +2352,6 @@ dependencies = [ "syn 2.0.72", ] -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.72", -] - [[package]] name = "serde_json" version = "1.0.121" @@ -2995,9 +2942,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.39.2" +version = "1.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" +checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" dependencies = [ "backtrace", "bytes", @@ -3097,20 +3044,35 @@ dependencies = [ "tokio", "tower-layer", "tower-service", +] + +[[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 0.1.2", + "tokio", + "tower-layer", + "tower-service", "tracing", ] [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -3176,6 +3138,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typeid" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e" + [[package]] name = "typenum" version = "1.17.0" diff --git a/Cargo.toml b/Cargo.toml index 06a58b1..e1e969c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,6 @@ cynic-querygen = "3.7.3" indoc = "2.0.5" [dependencies] -aide = { version = "0.13.4", features = ["scalar"] } anyhow = "1.0.86" axum = { version = "0.7.5", features = ["macros"] } bs58 = "0.5.1" @@ -24,8 +23,11 @@ cynic = { version = "3.7.3", features = ["http-reqwest-blocking"] } dashmap = "6.1.0" derive_more = { version = "1.0.0", features = ["full"] } dotenv = "0.15.0" -envy = "0.4.2" -futures = "0.3.30" +erased-serde = "0.4.5" +futures = "0.3.31" +http = "1.1.0" +http-body-util = "0.1.2" +mime = "0.3.17" paste = "1.0.15" pretty_assertions = "1.4.1" reqwest = { version = "0.12.5", features = ["json", "blocking"] } @@ -34,6 +36,7 @@ serde_json = { version = "1.0.121" } sqlx = { version = "0.8.0", features = ["runtime-tokio", "postgres", "json"] } thiserror = "1.0.63" tokio = { version = "1.39.2", features = ["full"] } +tower = "0.5.1" tracing = "0.1.40" tracing-subscriber = "0.3.18" diff --git a/src/api/search_transactions.rs b/src/api/search_transactions.rs index cabcb52..389d59d 100644 --- a/src/api/search_transactions.rs +++ b/src/api/search_transactions.rs @@ -513,9 +513,10 @@ impl From for BlockTransaction { // Operation 2: Account Creation Fee (if applicable) if let Some(creation_fee) = &user_command.creation_fee { + let negated_creation_fee = format!("-{}", creation_fee); operations.push(operation( operation_index, - Some(&format!("-{}", creation_fee)), + if user_command.status == TransactionStatus::Applied { Some(&negated_creation_fee) } else { None }, receiver_account_id, OperationType::AccountCreationFeeViaPayment, Some(&user_command.status), @@ -531,9 +532,10 @@ impl From for BlockTransaction { match user_command.command_type { // Operation 3: Payment Source Decrement UserCommandType::Payment => { + let negated_amt = format!("-{}", amt); operations.push(operation( operation_index, - Some(&format!("-{}", amt)), + if user_command.status == TransactionStatus::Applied { Some(&negated_amt) } else { None }, source_account_id, OperationType::PaymentSourceDec, Some(&user_command.status), @@ -547,7 +549,7 @@ impl From for BlockTransaction { // Operation 4: Payment Receiver Increment operations.push(operation( operation_index, - Some(&amt), + if user_command.status == TransactionStatus::Applied { Some(&amt) } else { None }, receiver_account_id, OperationType::PaymentReceiverInc, Some(&user_command.status), diff --git a/src/bin/mina-mesh.rs b/src/bin/mina-mesh.rs index ee7572a..0102b04 100644 --- a/src/bin/mina-mesh.rs +++ b/src/bin/mina-mesh.rs @@ -4,6 +4,7 @@ use anyhow::Result; use clap::Parser; use mina_mesh::{SearchTxOptimizationsCommand, ServeCommand}; +use tokio::{select, signal}; #[derive(Debug, Parser)] #[command(name = "mina-mesh", version, about = "A Mesh-compliant Server for Mina", propagate_version = true, author)] @@ -16,7 +17,31 @@ enum Command { async fn main() -> Result<()> { dotenv::dotenv().ok(); match Command::parse() { - Command::Serve(cmd) => cmd.run().await, + Command::Serve(cmd) => cmd.run(shutdown_signal()).await, Command::SearchTxOptimizations(cmd) => cmd.run().await, } } + +pub async fn shutdown_signal() { + let windows = async { + signal::ctrl_c().await.unwrap_or_else(|_| panic!("Error: failed to install windows shutdown handler")); + }; + + #[cfg(unix)] + let unix = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .unwrap_or_else(|_| panic!("Error: failed to install unix shutdown handler")) + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + select! { + () = windows => {}, + () = unix => {}, + } + + println!("Signal received - starting graceful shutdown..."); +} diff --git a/src/commands/serve.rs b/src/commands/serve.rs index b6dd7ce..7507b69 100644 --- a/src/commands/serve.rs +++ b/src/commands/serve.rs @@ -1,18 +1,11 @@ -use std::sync::Arc; +use std::future::Future; use anyhow::Result; -use axum::{ - debug_handler, - extract::State, - response::IntoResponse, - routing::{get, post}, - serve, Json, Router, -}; +use axum::serve; use clap::Args; -use paste::paste; use tokio::net::TcpListener; -use crate::{playground::handle_playground, util::Wrapper, MinaMesh, MinaMeshConfig, MinaMeshError}; +use crate::{create_router, MinaMeshConfig}; #[derive(Debug, Args)] #[command(about = "Start the Mina Mesh Server.")] @@ -29,87 +22,16 @@ pub struct ServeCommand { } impl ServeCommand { - pub async fn run(self) -> Result<()> { + pub async fn run(self, signal: F) -> Result<()> + where + F: Future + Send + 'static, + { tracing_subscriber::fmt::init(); let mina_mesh = self.config.to_mina_mesh().await?; - let mut router = Router::new() - .route("/account/balance", post(handle_account_balance)) - .route("/block", post(handle_block)) - .route("/call", post(handle_call)) - .route("/construction/combine", post(handle_construction_combine)) - .route("/construction/derive", post(handle_construction_derive)) - .route("/construction/hash", post(handle_construction_hash)) - .route("/construction/metadata", post(handle_construction_metadata)) - .route("/construction/parse", post(handle_construction_parse)) - .route("/construction/payloads", post(handle_construction_payloads)) - .route("/construction/preprocess", post(handle_construction_preprocess)) - .route("/construction/submit", post(handle_construction_submit)) - .route("/implemented_methods", get(handle_implemented_methods)) - .route("/mempool", post(handle_mempool)) - .route("/mempool/transaction", post(handle_mempool_transaction)) - .route("/network/list", post(handle_network_list)) - .route("/network/options", post(handle_network_options)) - .route("/network/status", post(handle_network_status)) - .route("/search/transactions", post(handle_search_transactions)) - .with_state(Arc::new(mina_mesh)); - if self.playground { - router = router.route("/", get(handle_playground)); - } + let router = create_router(mina_mesh, self.playground); let listener = TcpListener::bind(format!("{}:{}", self.host, self.port)).await?; tracing::info!("listening on http://{}", listener.local_addr()?); - serve(listener, router).await?; + serve(listener, router).with_graceful_shutdown(signal).await?; Ok(()) } } - -macro_rules! create_handler { - ($name:ident, $request_type:ty) => { - 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.map_err(MinaMeshError::from)) // Normalize errors to MinaMeshError - } - } - }; -} - -create_handler!(account_balance, AccountBalanceRequest); -create_handler!(block, BlockRequest); -create_handler!(call, CallRequest); -create_handler!(construction_combine, ConstructionCombineRequest); -create_handler!(construction_derive, ConstructionDeriveRequest); -create_handler!(construction_hash, ConstructionHashRequest); -create_handler!(construction_metadata, ConstructionMetadataRequest); -create_handler!(construction_parse, ConstructionParseRequest); -create_handler!(construction_payloads, ConstructionPayloadsRequest); -create_handler!(construction_preprocess, ConstructionPreprocessRequest); -create_handler!(construction_submit, ConstructionSubmitRequest); -create_handler!(mempool, NetworkRequest); -create_handler!(mempool_transaction, MempoolTransactionRequest); -create_handler!(network_list); -create_handler!(network_options, NetworkRequest); -create_handler!(network_status, NetworkRequest); -create_handler!(search_transactions, SearchTransactionsRequest); - -#[debug_handler] -async fn handle_implemented_methods() -> impl IntoResponse { - Json([ - "account_balance", - "block", - "mempool", - "mempool_transaction", - "network_list", - "network_options", - "network_status", - "search_transactions", - ]) -} diff --git a/src/create_router.rs b/src/create_router.rs new file mode 100644 index 0000000..67c0e0d --- /dev/null +++ b/src/create_router.rs @@ -0,0 +1,91 @@ +use std::sync::Arc; + +use axum::{ + debug_handler, + extract::State, + response::IntoResponse, + routing::{get, post}, + Json, Router, +}; +use paste::paste; + +use crate::{playground::handle_playground, util::Wrapper, MinaMesh, MinaMeshError}; + +pub fn create_router(mina_mesh: MinaMesh, playground: bool) -> Router { + let mut router = Router::new() + .route("/account/balance", post(handle_account_balance)) + .route("/block", post(handle_block)) + .route("/call", post(handle_call)) + .route("/construction/combine", post(handle_construction_combine)) + .route("/construction/derive", post(handle_construction_derive)) + .route("/construction/hash", post(handle_construction_hash)) + .route("/construction/metadata", post(handle_construction_metadata)) + .route("/construction/parse", post(handle_construction_parse)) + .route("/construction/payloads", post(handle_construction_payloads)) + .route("/construction/preprocess", post(handle_construction_preprocess)) + .route("/construction/submit", post(handle_construction_submit)) + .route("/implemented_methods", get(handle_implemented_methods)) + .route("/mempool", post(handle_mempool)) + .route("/mempool/transaction", post(handle_mempool_transaction)) + .route("/network/list", post(handle_network_list)) + .route("/network/options", post(handle_network_options)) + .route("/network/status", post(handle_network_status)) + .route("/search/transactions", post(handle_search_transactions)) + .with_state(Arc::new(mina_mesh)); + if playground { + router = router.route("/", get(handle_playground)); + } + router +} + +macro_rules! create_handler { + ($name:ident, $request_type:ty) => { + 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.map_err(MinaMeshError::from)) // Normalize errors to MinaMeshError + } + } + }; +} + +create_handler!(account_balance, AccountBalanceRequest); +create_handler!(block, BlockRequest); +create_handler!(call, CallRequest); +create_handler!(construction_combine, ConstructionCombineRequest); +create_handler!(construction_derive, ConstructionDeriveRequest); +create_handler!(construction_hash, ConstructionHashRequest); +create_handler!(construction_metadata, ConstructionMetadataRequest); +create_handler!(construction_parse, ConstructionParseRequest); +create_handler!(construction_payloads, ConstructionPayloadsRequest); +create_handler!(construction_preprocess, ConstructionPreprocessRequest); +create_handler!(construction_submit, ConstructionSubmitRequest); +create_handler!(mempool, NetworkRequest); +create_handler!(mempool_transaction, MempoolTransactionRequest); +create_handler!(network_list); +create_handler!(network_options, NetworkRequest); +create_handler!(network_status, NetworkRequest); +create_handler!(search_transactions, SearchTransactionsRequest); + +#[debug_handler] +async fn handle_implemented_methods() -> impl IntoResponse { + Json([ + "account_balance", + "block", + "mempool", + "mempool_transaction", + "network_list", + "network_options", + "network_status", + "search_transactions", + ]) +} diff --git a/src/error.rs b/src/error.rs index 804351f..b928f28 100644 --- a/src/error.rs +++ b/src/error.rs @@ -348,6 +348,12 @@ impl From for MinaMeshError { } } +impl From for MinaMeshError { + fn from(value: reqwest::Error) -> Self { + MinaMeshError::Exception(value.to_string()) + } +} + impl From for coinbase_mesh::models::Error { fn from(error: MinaMeshError) -> Self { coinbase_mesh::models::Error { diff --git a/src/lib.rs b/src/lib.rs index 88eac73..32bccd5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,13 +1,15 @@ mod api; mod commands; mod config; +mod create_router; mod error; mod graphql; mod operation; mod playground; mod sql_to_mesh; +pub mod test; mod types; -mod util; +pub mod util; use std::time::{Duration, Instant}; @@ -15,6 +17,7 @@ pub use coinbase_mesh::models; use coinbase_mesh::models::BlockIdentifier; pub use commands::*; pub use config::*; +pub use create_router::create_router; use dashmap::DashMap; pub use error::*; use graphql::GraphQLClient; diff --git a/src/test.rs b/src/test.rs new file mode 100644 index 0000000..162aa56 --- /dev/null +++ b/src/test.rs @@ -0,0 +1,137 @@ +use std::fmt::Display; + +use anyhow::Result; +use axum::{ + body::{to_bytes, Body}, + http::{Request, StatusCode}, + response::IntoResponse, + Router, +}; +use pretty_assertions::assert_eq; +use reqwest::Client; +use serde_json::{Map, Value}; +use tower::ServiceExt; + +use crate::{create_router, MinaMesh}; + +pub struct ResponseComparisonContext { + pub router: Router, + pub client: Client, + pub endpoint: String, +} + +impl ResponseComparisonContext { + pub fn new(mina_mesh: MinaMesh, endpoint: String) -> Self { + let client = Client::new(); + let router = create_router(mina_mesh, false); + Self { client, endpoint, router } + } + + pub async fn assert_responses_eq(&self, subpath: &str, maybe_body_bytes: Option>) -> 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))?; + assert_eq!(a, b, "Mismatch for {subpath}; left = mina_mesh, right = rosetta"); + Ok(()) + } + + async fn mina_mesh_req(&self, subpath: &str, body_bytes: Vec) -> Result { + let oneshot_req = Request::builder() + .method("POST") + .uri(subpath) + .header(http::header::CONTENT_TYPE, "application/json") + .body(Body::from(body_bytes))?; + let response = self.router.clone().oneshot(oneshot_req).await.into_response(); + let status = response.status(); + let body_raw = String::from_utf8(to_bytes(response.into_body(), 100_000).await?.to_vec())?; + let body = normalize_body(body_raw.as_str())?; + if status == StatusCode::OK { + Ok(body) + } else { + Ok(ErrorContainer { status: status.to_string(), body }.to_string()) + } + } + + async fn legacy_req(&self, subpath: &str, body_bytes: Vec) -> Result { + let response = self.client.post(format!("{}{subpath}", self.endpoint)).body(body_bytes).send().await?; + let status = response.status(); + let body = normalize_body(&response.text().await?)?; + if status == StatusCode::OK { + Ok(body) + } else { + Ok(ErrorContainer { status: status.to_string(), body }.to_string()) + } + } +} + +#[derive(Debug, PartialEq)] +struct ErrorContainer { + status: String, + body: String, +} + +impl Display for ErrorContainer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}: {}", self.status, self.body) + } +} + +fn normalize_body(raw: &str) -> Result { + let mut json_unsorted: Value = serde_json::from_str(raw)?; + sort_json_value(&mut json_unsorted); + remove_empty_related_transactions(&mut json_unsorted); + Ok(serde_json::to_string_pretty(&json_unsorted)?) +} + +fn sort_json_value(value: &mut Value) { + match value { + Value::Object(map) => { + let mut keys: Vec<_> = map.keys().cloned().collect(); + keys.sort(); + let mut sorted_map = Map::new(); + for k in keys { + if let Some(mut v) = map.remove(&k) { + sort_json_value(&mut v); + sorted_map.insert(k, v); + } + } + *map = sorted_map; + } + Value::Array(vec) => { + for v in vec.iter_mut() { + sort_json_value(v); + } + } + _ => {} + } +} + +// Remove empty "related_transactions" arrays from the JSON +// This is necessary because Rosetta OCaml includes empty arrays in the response +// but mina-mesh does not +// Workaround for https://github.com/MinaFoundation/MinaMesh/issues/48 +fn remove_empty_related_transactions(value: &mut Value) { + match value { + Value::Object(map) => { + map.retain( + |key, v| { + if key == "related_transactions" { + !matches!(v, Value::Array(arr) if arr.is_empty()) + } else { + true + } + }, + ); + + for v in map.values_mut() { + remove_empty_related_transactions(v); + } + } + Value::Array(vec) => { + for v in vec.iter_mut() { + remove_empty_related_transactions(v); + } + } + _ => {} + } +} diff --git a/tests/account_balance.rs b/tests/account_balance.rs index 4bc4699..4780aaf 100644 --- a/tests/account_balance.rs +++ b/tests/account_balance.rs @@ -27,7 +27,7 @@ async fn responses() -> Result<()> { currencies: None, network_identifier: Box::new(NetworkIdentifier { blockchain: "mina".into(), - network: "testnet".into(), + network: "devnet".into(), sub_network_identifier: None, }), }) @@ -53,7 +53,7 @@ async fn account_not_found_error() -> Result<()> { currencies: None, network_identifier: Box::new(NetworkIdentifier { blockchain: "mina".into(), - network: "testnet".into(), + network: "devnet".into(), sub_network_identifier: None, }), }) diff --git a/tests/block.rs b/tests/block.rs index 0adbd86..7f0695a 100644 --- a/tests/block.rs +++ b/tests/block.rs @@ -46,7 +46,7 @@ fn specified_identifiers() -> &'static [PartialBlockIdentifier; 3] { fn network_identifier() -> &'static NetworkIdentifier { static NETWORK_IDENTIFIER: OnceLock = OnceLock::new(); - NETWORK_IDENTIFIER.get_or_init(|| NetworkIdentifier::new("mina".to_string(), "testnet".to_string())) + NETWORK_IDENTIFIER.get_or_init(|| NetworkIdentifier::new("mina".to_string(), "devnet".to_string())) } #[tokio::test] diff --git a/tests/compare_to_ocaml.rs b/tests/compare_to_ocaml.rs new file mode 100644 index 0000000..2a52c47 --- /dev/null +++ b/tests/compare_to_ocaml.rs @@ -0,0 +1,25 @@ +mod fixtures; + +use anyhow::Result; +use futures::future::join_all; +use mina_mesh::{test::ResponseComparisonContext, MinaMeshConfig}; +use serde::Serialize; + +const LEGACY_ENDPOINT: &str = "https://rosetta-devnet.minaprotocol.network"; + +async fn compare_responses(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(()) +} + +#[tokio::test] +async fn search_transactions() -> Result<()> { + let (subpath, reqs) = fixtures::search_transactions(); + compare_responses(subpath, &reqs).await +} diff --git a/tests/fixtures/account_balance.rs b/tests/fixtures/account_balance.rs new file mode 100644 index 0000000..e35dba4 --- /dev/null +++ b/tests/fixtures/account_balance.rs @@ -0,0 +1,31 @@ +use mina_mesh::models::{AccountBalanceRequest, AccountIdentifier, NetworkIdentifier, PartialBlockIdentifier}; + +use super::CompareGroup; + +#[allow(dead_code)] +pub fn account_balance<'a>() -> CompareGroup<'a> { + ("/account/balance", vec![ + Box::new(AccountBalanceRequest { + account_identifier: Box::new(AccountIdentifier::new( + "B62qmo4nfFemr9hFtvz8F5h4JFSCxikVNsUJmZcfXQ9SGJ4abEC1RtH".to_string(), + )), + block_identifier: Some(Box::new(PartialBlockIdentifier { index: Some(100), hash: None })), + network_identifier: Box::new(NetworkIdentifier::new("mina".to_string(), "devnet".to_string())), + currencies: None, + }), + Box::new(AccountBalanceRequest { + account_identifier: Box::new(AccountIdentifier { + address: "B62qkYHGYmws5CYa3phYEKoZvrENTegEhUJYMhzHUQe5UZwCdWob8zv".to_string(), + sub_account: None, + metadata: None, + }), + block_identifier: Some(Box::new(PartialBlockIdentifier { index: Some(6265), hash: None })), + currencies: None, + network_identifier: Box::new(NetworkIdentifier { + blockchain: "mina".into(), + network: "devnet".into(), + sub_network_identifier: None, + }), + }), + ]) +} diff --git a/tests/fixtures/block.rs b/tests/fixtures/block.rs new file mode 100644 index 0000000..623500f --- /dev/null +++ b/tests/fixtures/block.rs @@ -0,0 +1,17 @@ +use mina_mesh::models::{BlockRequest, NetworkIdentifier, PartialBlockIdentifier}; + +use super::CompareGroup; + +#[allow(dead_code)] +pub fn block<'a>() -> CompareGroup<'a> { + ("/block", vec![ + Box::new(BlockRequest { + network_identifier: Box::new(NetworkIdentifier::new("mina".to_string(), "devnet".to_string())), + block_identifier: Box::new(PartialBlockIdentifier::new()), + }), + Box::new(BlockRequest { + network_identifier: Box::new(NetworkIdentifier::new("mina".to_string(), "devnet".to_string())), + block_identifier: Box::new(PartialBlockIdentifier { index: Some(52676), hash: None }), + }), + ]) +} diff --git a/tests/fixtures/mod.rs b/tests/fixtures/mod.rs new file mode 100644 index 0000000..53e61be --- /dev/null +++ b/tests/fixtures/mod.rs @@ -0,0 +1,13 @@ +use erased_serde::Serialize as ErasedSerialize; + +mod account_balance; +mod block; +mod search_transactions; + +#[allow(unused_imports)] +pub use account_balance::*; +#[allow(unused_imports)] +pub use block::*; +pub use search_transactions::*; + +pub type CompareGroup<'a> = (&'a str, Vec>); diff --git a/tests/fixtures/search_transactions.rs b/tests/fixtures/search_transactions.rs new file mode 100644 index 0000000..303ddfb --- /dev/null +++ b/tests/fixtures/search_transactions.rs @@ -0,0 +1,41 @@ +use mina_mesh::models::{NetworkIdentifier, SearchTransactionsRequest, TransactionIdentifier}; + +use super::CompareGroup; + +pub fn search_transactions<'a>() -> CompareGroup<'a> { + ("/search/transactions", vec![ + Box::new(SearchTransactionsRequest { + network_identifier: Box::new(NetworkIdentifier::new("mina".to_string(), "devnet".to_string())), + address: Some("B62qkd6yYALkQMq2SFd5B57bJbGBMA2QuGtLPMzRhhnvexRtVRycZWP".to_string()), + limit: Some(5), + offset: Some(0), + ..Default::default() + }), + Box::new(SearchTransactionsRequest { + network_identifier: Box::new(NetworkIdentifier::new("mina".to_string(), "devnet".to_string())), + max_block: Some(44), + status: Some("failed".to_string()), + limit: Some(5), + ..Default::default() + }), + Box::new(SearchTransactionsRequest { + network_identifier: Box::new(NetworkIdentifier::new("mina".to_string(), "devnet".to_string())), + max_block: Some(44), + transaction_identifier: Some(Box::new(TransactionIdentifier::new( + // cspell:disable-next-line + "CkpYcKc2oGs8JUd4tmdGBsZXQCQVkayuyffEjrNWctX5Wuad3vVNe".to_string(), + ))), + limit: Some(5), + ..Default::default() + }), + Box::new(SearchTransactionsRequest { + network_identifier: Box::new(NetworkIdentifier::new("mina".to_string(), "devnet".to_string())), + transaction_identifier: Some(Box::new(TransactionIdentifier::new( + // cspell:disable-next-line + "5JvFoEyvuPu9zmi4bDGbhqsakre2SPQU1KKbeh2Lk5uC9eYrc2h2".to_string(), + ))), + limit: Some(1), + ..Default::default() + }), + ]) +} diff --git a/tests/network_options.rs b/tests/network_options.rs index eb140bd..c5cd5e1 100644 --- a/tests/network_options.rs +++ b/tests/network_options.rs @@ -7,7 +7,7 @@ use mina_mesh::{ #[tokio::test] async fn test_network_options() -> Result<()> { - let req = NetworkRequest::new(NetworkIdentifier::new("mina".to_string(), "testnet".to_string())); + let req = NetworkRequest::new(NetworkIdentifier::new("mina".to_string(), "devnet".to_string())); let response = MinaMeshConfig::from_env().to_mina_mesh().await?.network_options(req).await?; assert_debug_snapshot!(&response.allow); Ok(()) diff --git a/tests/search_transactions.rs b/tests/search_transactions.rs index 54e868e..e8cec90 100644 --- a/tests/search_transactions.rs +++ b/tests/search_transactions.rs @@ -12,7 +12,7 @@ async fn search_transactions_specified() -> Result<()> { let address = "B62qkd6yYALkQMq2SFd5B57bJbGBMA2QuGtLPMzRhhnvexRtVRycZWP"; let request_addr = SearchTransactionsRequest { - network_identifier: Box::new(NetworkIdentifier::new("mina".to_string(), "testnet".to_string())), + network_identifier: Box::new(NetworkIdentifier::new("mina".to_string(), "devnet".to_string())), address: Some(address.to_string()), limit: Some(5), offset: Some(0), @@ -22,7 +22,7 @@ async fn search_transactions_specified() -> Result<()> { let response_addr = mina_mesh.search_transactions(request_addr).await; let request_acct_id = SearchTransactionsRequest { - network_identifier: Box::new(NetworkIdentifier::new("mina".to_string(), "testnet".to_string())), + network_identifier: Box::new(NetworkIdentifier::new("mina".to_string(), "devnet".to_string())), account_identifier: Some(Box::new(AccountIdentifier::new(address.to_string()))), limit: Some(5), offset: Some(0), @@ -42,7 +42,7 @@ async fn search_transactions_failed() -> Result<()> { let mina_mesh = MinaMeshConfig::from_env().to_mina_mesh().await?; let request = SearchTransactionsRequest { - network_identifier: Box::new(NetworkIdentifier::new("mina".to_string(), "testnet".to_string())), + network_identifier: Box::new(NetworkIdentifier::new("mina".to_string(), "devnet".to_string())), max_block: Some(44), status: Some("failed".to_string()), limit: Some(5), @@ -61,7 +61,7 @@ async fn search_transactions_internal_command() -> Result<()> { let mina_mesh = MinaMeshConfig::from_env().to_mina_mesh().await?; let request = SearchTransactionsRequest { - network_identifier: Box::new(NetworkIdentifier::new("mina".to_string(), "testnet".to_string())), + network_identifier: Box::new(NetworkIdentifier::new("mina".to_string(), "devnet".to_string())), max_block: Some(44), transaction_identifier: Some(Box::new(TransactionIdentifier::new( // cspell:disable-next-line @@ -83,7 +83,7 @@ async fn search_transactions_zkapp_success() -> Result<()> { let mina_mesh = MinaMeshConfig::from_env().to_mina_mesh().await?; let request = SearchTransactionsRequest { - network_identifier: Box::new(NetworkIdentifier::new("mina".to_string(), "testnet".to_string())), + network_identifier: Box::new(NetworkIdentifier::new("mina".to_string(), "devnet".to_string())), transaction_identifier: Some(Box::new(TransactionIdentifier::new( // cspell:disable-next-line "5JvFoEyvuPu9zmi4bDGbhqsakre2SPQU1KKbeh2Lk5uC9eYrc2h2".to_string(), @@ -104,7 +104,7 @@ async fn search_transactions_zkapp_failed() -> Result<()> { let mina_mesh = MinaMeshConfig::from_env().to_mina_mesh().await?; let request = SearchTransactionsRequest { - network_identifier: Box::new(NetworkIdentifier::new("mina".to_string(), "testnet".to_string())), + network_identifier: Box::new(NetworkIdentifier::new("mina".to_string(), "devnet".to_string())), transaction_identifier: Some(Box::new(TransactionIdentifier::new( // cspell:disable-next-line "5JujBt8rnKheA7CHBnTwUDXrHtQxqPB9LL5Q8y4KwLjPBsBSJuSE".to_string(), @@ -132,7 +132,7 @@ async fn search_transactions_zkapp_tokens_account_identifier() -> Result<()> { let metadata = serde_json::json!({ "token_id": token_id }); let request_address1_token = SearchTransactionsRequest { - network_identifier: Box::new(NetworkIdentifier::new("mina".to_string(), "testnet".to_string())), + network_identifier: Box::new(NetworkIdentifier::new("mina".to_string(), "devnet".to_string())), max_block: Some(max_block), account_identifier: Some(Box::new(AccountIdentifier { address: address1.to_string(), @@ -145,7 +145,7 @@ async fn search_transactions_zkapp_tokens_account_identifier() -> Result<()> { let response_address1_token = mina_mesh.search_transactions(request_address1_token).await; let request_address2_token = SearchTransactionsRequest { - network_identifier: Box::new(NetworkIdentifier::new("mina".to_string(), "testnet".to_string())), + network_identifier: Box::new(NetworkIdentifier::new("mina".to_string(), "devnet".to_string())), max_block: Some(max_block), account_identifier: Some(Box::new(AccountIdentifier { address: address2.to_string(), @@ -168,7 +168,7 @@ async fn search_transactions_zkapp_tokens_tx_hash() -> Result<()> { let mina_mesh = MinaMeshConfig::from_env().to_mina_mesh().await?; let request_tx_hash = SearchTransactionsRequest { - network_identifier: Box::new(NetworkIdentifier::new("mina".to_string(), "testnet".to_string())), + network_identifier: Box::new(NetworkIdentifier::new("mina".to_string(), "devnet".to_string())), transaction_identifier: Some(Box::new(TransactionIdentifier::new( // cspell:disable-next-line "5JuotEHhjuYbu2oucyTiVhJX3Abx5DPL4NXnM7CP9hfJZLE5G8n9".to_string(), diff --git a/tests/snapshots/network_list__network_list.snap b/tests/snapshots/network_list__network_list.snap index 08a1841..6fb6b0a 100644 --- a/tests/snapshots/network_list__network_list.snap +++ b/tests/snapshots/network_list__network_list.snap @@ -5,7 +5,7 @@ expression: "&response.network_identifiers" [ NetworkIdentifier { blockchain: "mina", - network: "testnet", + network: "devnet", sub_network_identifier: None, }, ] diff --git a/tests/snapshots/search_transactions__search_transactions_failed.snap b/tests/snapshots/search_transactions__search_transactions_failed.snap index d4b3418..7861889 100644 --- a/tests/snapshots/search_transactions__search_transactions_failed.snap +++ b/tests/snapshots/search_transactions__search_transactions_failed.snap @@ -75,17 +75,7 @@ Ok( ), }, ), - amount: Some( - Amount { - value: "-1500000", - currency: Currency { - symbol: "MINA", - decimals: 9, - metadata: None, - }, - metadata: None, - }, - ), + amount: None, coin_change: None, metadata: Some( Object { @@ -121,17 +111,7 @@ Ok( ), }, ), - amount: Some( - Amount { - value: "1500000", - currency: Currency { - symbol: "MINA", - decimals: 9, - metadata: None, - }, - metadata: None, - }, - ), + amount: None, coin_change: None, metadata: Some( Object { @@ -219,17 +199,7 @@ Ok( ), }, ), - amount: Some( - Amount { - value: "-1500000", - currency: Currency { - symbol: "MINA", - decimals: 9, - metadata: None, - }, - metadata: None, - }, - ), + amount: None, coin_change: None, metadata: Some( Object { @@ -265,17 +235,7 @@ Ok( ), }, ), - amount: Some( - Amount { - value: "1500000", - currency: Currency { - symbol: "MINA", - decimals: 9, - metadata: None, - }, - metadata: None, - }, - ), + amount: None, coin_change: None, metadata: Some( Object { @@ -363,17 +323,7 @@ Ok( ), }, ), - amount: Some( - Amount { - value: "-1500000", - currency: Currency { - symbol: "MINA", - decimals: 9, - metadata: None, - }, - metadata: None, - }, - ), + amount: None, coin_change: None, metadata: Some( Object { @@ -409,17 +359,7 @@ Ok( ), }, ), - amount: Some( - Amount { - value: "1500000", - currency: Currency { - symbol: "MINA", - decimals: 9, - metadata: None, - }, - metadata: None, - }, - ), + amount: None, coin_change: None, metadata: Some( Object { @@ -507,17 +447,7 @@ Ok( ), }, ), - amount: Some( - Amount { - value: "-1500000", - currency: Currency { - symbol: "MINA", - decimals: 9, - metadata: None, - }, - metadata: None, - }, - ), + amount: None, coin_change: None, metadata: Some( Object { @@ -553,17 +483,7 @@ Ok( ), }, ), - amount: Some( - Amount { - value: "1500000", - currency: Currency { - symbol: "MINA", - decimals: 9, - metadata: None, - }, - metadata: None, - }, - ), + amount: None, coin_change: None, metadata: Some( Object { @@ -651,17 +571,7 @@ Ok( ), }, ), - amount: Some( - Amount { - value: "-1500000", - currency: Currency { - symbol: "MINA", - decimals: 9, - metadata: None, - }, - metadata: None, - }, - ), + amount: None, coin_change: None, metadata: Some( Object { @@ -697,17 +607,7 @@ Ok( ), }, ), - amount: Some( - Amount { - value: "1500000", - currency: Currency { - symbol: "MINA", - decimals: 9, - metadata: None, - }, - metadata: None, - }, - ), + amount: None, coin_change: None, metadata: Some( Object { diff --git a/tests/validate_network.rs b/tests/validate_network.rs index e7ad040..ce03e1e 100644 --- a/tests/validate_network.rs +++ b/tests/validate_network.rs @@ -15,13 +15,13 @@ async fn genesis_block_identifier() -> Result<()> { #[tokio::test] async fn validate_network_ok() -> Result<()> { let mina_mesh = MinaMeshConfig::from_env().to_mina_mesh().await?; - let network = NetworkIdentifier::new("mina".to_string(), "testnet".to_string()); + let network = NetworkIdentifier::new("mina".to_string(), "devnet".to_string()); assert!(mina_mesh.get_from_cache(NetworkId).is_none(), "Cache should be empty"); let result = mina_mesh.validate_network(&network).await; assert!(result.is_ok(), "validate_network failed"); if let Some(cached_network_id) = mina_mesh.get_from_cache(NetworkId) { - assert_eq!(cached_network_id, "mina:testnet", "Cached network_id does not match"); + assert_eq!(cached_network_id, "mina:devnet", "Cached network_id does not match"); } else { panic!("Cache was not updated after validate_network"); } @@ -36,7 +36,7 @@ async fn validate_network_err() -> Result<()> { assert!(result.is_err(), "validate_network should have failed"); if let Err(MinaMeshError::NetworkDne(expected, actual)) = result { assert_eq!(expected, "mina:unknown"); - assert_eq!(actual, "mina:testnet"); + assert_eq!(actual, "mina:devnet"); } else { panic!("Unexpected error type"); } diff --git a/words.txt b/words.txt index 653be63..f177e53 100644 --- a/words.txt +++ b/words.txt @@ -31,12 +31,14 @@ minafoundation MINAMESH navroot nocapture +oneshot openai openapi piotr preprocess qnighy querygen +reqs reqwest retriable RUSTFLAGS