diff --git a/Cargo.lock b/Cargo.lock index 4234fc73e1d99..daec9eccbd19d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -160,7 +160,7 @@ dependencies = [ "aptos-vm", "aptosdb", "async-trait", - "base64", + "base64 0.13.0", "bcs", "cached-framework-packages", "clap 3.2.11", @@ -191,7 +191,7 @@ dependencies = [ [[package]] name = "aptos-api" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "aptos-api-types", @@ -211,7 +211,7 @@ dependencies = [ "aptos-vm", "aptosdb", "bcs", - "bytes", + "bytes 1.1.0", "cached-framework-packages", "executor", "executor-types", @@ -221,9 +221,12 @@ dependencies = [ "hex", "hyper", "mempool-notifications", + "mime", "move-deps", "once_cell", "percent-encoding", + "poem", + "poem-openapi", "proptest", "rand 0.7.3", "regex", @@ -232,8 +235,10 @@ dependencies = [ "serde_json", "storage-interface", "tokio", + "url", "vm-validator", "warp", + "warp-reverse-proxy", ] [[package]] @@ -243,13 +248,18 @@ dependencies = [ "anyhow", "aptos-config", "aptos-crypto", + "aptos-openapi", "aptos-state-view", "aptos-transaction-builder", "aptos-types", "aptos-vm", + "async-trait", "bcs", "hex", + "mime", "move-deps", + "poem", + "poem-openapi", "serde 1.0.137", "serde_json", "warp", @@ -281,6 +291,7 @@ dependencies = [ "bcs", "get_if_addrs", "mirai-annotations", + "poem-openapi", "rand 0.7.3", "serde 1.0.137", "serde_yaml", @@ -298,7 +309,7 @@ dependencies = [ "bitvec", "blst", "byteorder", - "bytes", + "bytes 1.1.0", "criterion", "curve25519-dalek", "digest 0.9.0", @@ -379,7 +390,7 @@ dependencies = [ "aptos-rest-client", "aptos-sdk", "bcs", - "bytes", + "bytes 1.1.0", "clap 3.2.11", "futures", "hex", @@ -407,7 +418,7 @@ dependencies = [ "aptos-rest-client", "aptos-sdk", "bcs", - "bytes", + "bytes 1.1.0", "clap 3.2.11", "futures", "hex", @@ -502,7 +513,7 @@ dependencies = [ name = "aptos-github-client" version = "0.1.0" dependencies = [ - "base64", + "base64 0.13.0", "proxy", "serde 1.0.137", "serde_json", @@ -768,6 +779,18 @@ dependencies = [ "url", ] +[[package]] +name = "aptos-openapi" +version = "0.1.0" +dependencies = [ + "async-trait", + "mime", + "poem", + "poem-openapi", + "serde 1.0.137", + "serde_json", +] + [[package]] name = "aptos-operational-tool" version = "0.1.0" @@ -783,7 +806,7 @@ dependencies = [ "aptos-temppath", "aptos-transaction-builder", "aptos-types", - "base64", + "base64 0.13.0", "bcs", "futures", "hex", @@ -979,7 +1002,7 @@ dependencies = [ "aptos-temppath", "aptos-time-service", "aptos-vault-client", - "base64", + "base64 0.13.0", "bcs", "chrono", "enum_dispatch", @@ -1118,6 +1141,7 @@ dependencies = [ "num-derive", "num-traits 0.2.15", "once_cell", + "poem-openapi", "proptest", "proptest-derive", "rand 0.7.3", @@ -1150,7 +1174,7 @@ dependencies = [ "aptos-crypto", "aptos-proptest-helpers", "aptos-types", - "base64", + "base64 0.13.0", "chrono", "native-tls", "once_cell", @@ -1381,7 +1405,7 @@ dependencies = [ "async-trait", "backup-service", "bcs", - "bytes", + "bytes 1.1.0", "executor", "executor-test-helpers", "executor-types", @@ -1419,7 +1443,7 @@ dependencies = [ "aptos-types", "aptosdb", "bcs", - "bytes", + "bytes 1.1.0", "hyper", "once_cell", "reqwest", @@ -1435,6 +1459,12 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc19a4937b4fbd3fe3379793130e42060d10627a360f2127802b10b87e7baf74" +[[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" + [[package]] name = "base64" version = "0.13.0" @@ -1677,6 +1707,12 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +[[package]] +name = "bytes" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" + [[package]] name = "bytes" version = "1.1.0" @@ -1996,7 +2032,7 @@ version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a604e93b79d1808327a6fca85a6f2d69de66461e7620f5a4cbf5fb4d1d7c948" dependencies = [ - "bytes", + "bytes 1.1.0", "memchr", ] @@ -2034,7 +2070,7 @@ dependencies = [ "async-trait", "bcs", "byteorder", - "bytes", + "bytes 1.1.0", "channel", "claim", "consensus-notifications", @@ -2168,7 +2204,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05" dependencies = [ "aes-gcm", - "base64", + "base64 0.13.0", "hkdf 0.12.3", "hmac 0.12.1", "percent-encoding", @@ -2501,9 +2537,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.13.4" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +checksum = "4529658bdda7fd6769b8614be250cdcfc3aeb0ee72fe66f9e41e5e5eb73eac02" dependencies = [ "darling_core", "darling_macro", @@ -2511,9 +2547,9 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.13.4" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +checksum = "649c91bc01e8b1eac09fb91e8dbc7d517684ca6be8ebc75bb9cafc894f9fdb6f" dependencies = [ "fnv", "ident_case", @@ -2525,9 +2561,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.13.4" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +checksum = "ddfc69c5bfcbd2fc09a0f38451d2daf0e372e367986a83906d1b0dbc88134fb5" dependencies = [ "darling_core", "quote 1.0.18", @@ -3588,7 +3624,7 @@ version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" dependencies = [ - "bytes", + "bytes 1.1.0", "fnv", "futures-core", "futures-sink", @@ -3668,9 +3704,9 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d" dependencies = [ - "base64", + "base64 0.13.0", "bitflags", - "bytes", + "bytes 1.1.0", "headers-core", "http", "httpdate", @@ -3802,7 +3838,7 @@ version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff8670570af52249509a86f5e3e18a08c60b177071826898fde8997cf5f6bfbb" dependencies = [ - "bytes", + "bytes 1.1.0", "fnv", "itoa 1.0.2", ] @@ -3813,7 +3849,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" dependencies = [ - "bytes", + "bytes 1.1.0", "http", "pin-project-lite", ] @@ -3867,7 +3903,7 @@ version = "0.14.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2" dependencies = [ - "bytes", + "bytes 1.1.0", "futures-channel", "futures-core", "futures-util", @@ -3903,7 +3939,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ - "bytes", + "bytes 1.1.0", "hyper", "native-tls", "tokio", @@ -4184,8 +4220,8 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc1f973542059e6d5a6d63de6a9539d0ec784f82b2327f3c1915d33200bc6a4" dependencies = [ - "base64", - "bytes", + "base64 0.13.0", + "bytes 1.1.0", "chrono", "serde 1.0.137", "serde-value", @@ -4214,8 +4250,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d47a55e9f881dc5027dcaf026670fa24b41f67926ab6517e2155488fe9c012a" dependencies = [ "Inflector", - "base64", - "bytes", + "base64 0.13.0", + "bytes 1.1.0", "chrono", "dirs-next", "either", @@ -4377,7 +4413,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95b09eff1b35ed3b33b877ced3a691fc7a481919c7e29c53c906226fcf55e2a1" dependencies = [ "arrayref", - "base64", + "base64 0.13.0", "digest 0.9.0", "hmac-drbg", "libsecp256k1-core", @@ -4525,7 +4561,7 @@ name = "memsocket" version = "0.1.0" dependencies = [ "aptos-infallible", - "bytes", + "bytes 1.1.0", "futures", "once_cell", ] @@ -5356,7 +5392,7 @@ version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f8f35e687561d5c1667590911e6698a8cb714a134a7505718a182e7bc9d3836" dependencies = [ - "bytes", + "bytes 1.1.0", "encoding_rs", "futures-util", "http", @@ -5441,7 +5477,7 @@ name = "netcore" version = "0.1.0" dependencies = [ "aptos-types", - "bytes", + "bytes 1.1.0", "futures", "memsocket", "pin-project", @@ -5471,7 +5507,7 @@ dependencies = [ "aptos-types", "async-trait", "bcs", - "bytes", + "bytes 1.1.0", "channel", "futures", "futures-util", @@ -6090,7 +6126,7 @@ dependencies = [ "aptos-types", "bcs", "bounded-executor", - "bytes", + "bytes 1.1.0", "channel", "futures", "netcore", @@ -6118,7 +6154,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb" dependencies = [ - "base64", + "base64 0.13.0", "once_cell", "regex", ] @@ -6299,13 +6335,13 @@ dependencies = [ [[package]] name = "poem" -version = "1.3.31" +version = "1.3.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61fc6256b0e7050d11d3ac5d8109eb87c7939189ecc80228c92b6cb00f29dd87" +checksum = "01e479ed477ea11939bea754109eaa024e55bf1639e19c1753fb250026351da8" dependencies = [ "anyhow", "async-trait", - "bytes", + "bytes 1.1.0", "chrono", "cookie 0.16.0", "futures-util", @@ -6319,6 +6355,8 @@ dependencies = [ "pin-project-lite", "poem-derive", "regex", + "rfc7239", + "rustls-pemfile", "serde 1.0.137", "serde_json", "serde_urlencoded", @@ -6327,16 +6365,18 @@ dependencies = [ "thiserror", "time 0.3.9", "tokio", + "tokio-rustls 0.23.4", "tokio-stream", "tokio-util 0.7.2", "tracing", + "typed-headers", ] [[package]] name = "poem-derive" -version = "1.3.31" +version = "1.3.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb23ee18a9b915a04d1ee2e7dc6aa77e1e50e067c0aba2b25b623af2b1f5cf2d" +checksum = "6d454ac05b5299eb8e6261214fcb823f0c3c1b02f47167f8809c9dc2db0ee033" dependencies = [ "proc-macro-crate", "proc-macro2 1.0.39", @@ -6346,12 +6386,12 @@ dependencies = [ [[package]] name = "poem-openapi" -version = "2.0.1" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9763db3864695e75763cd4df7d91e6ece663e8cb6cd28466ed83f3bb2218503f" +checksum = "c27cd1c97d45a9552b0c80bab1d51f93a1050c3f7f91e9312239b3b87c336b0f" dependencies = [ - "base64", - "bytes", + "base64 0.13.0", + "bytes 1.1.0", "derive_more", "futures-util", "mime", @@ -6370,9 +6410,9 @@ dependencies = [ [[package]] name = "poem-openapi-derive" -version = "2.0.1" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66c7322903b05cfa23434e8915a52f32f162d7d2b55b1282b392346854d0c19b" +checksum = "a5105e426a8a747fefc4be06ab7705cb63bc3ab1b2db373867e1d83b1d95877a" dependencies = [ "Inflector", "darling", @@ -6928,8 +6968,8 @@ version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb" dependencies = [ - "base64", - "bytes", + "base64 0.13.0", + "bytes 1.1.0", "cookie 0.15.1", "cookie_store", "encoding_rs", @@ -7010,6 +7050,15 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "rfc7239" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "087317b3cf7eb481f13bd9025d729324b7cd068d6f470e2d76d049e191f5ba47" +dependencies = [ + "uncased", +] + [[package]] name = "ring" version = "0.16.20" @@ -7077,11 +7126,32 @@ version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" dependencies = [ - "base64", + "base64 0.13.0", + "log", + "ring", + "sct 0.6.1", + "webpki 0.21.4", +] + +[[package]] +name = "rustls" +version = "0.20.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033" +dependencies = [ "log", "ring", - "sct", - "webpki", + "sct 0.7.0", + "webpki 0.22.0", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7522c9de787ff061458fe9a829dc790a3f5b22dc571694fc5883f448b94d9a9" +dependencies = [ + "base64 0.13.0", ] [[package]] @@ -7218,6 +7288,16 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "security-framework" version = "2.6.1" @@ -7693,7 +7773,7 @@ dependencies = [ "aptosdb", "async-trait", "backup-cli", - "base64", + "base64 0.13.0", "bcs", "cached-framework-packages", "consensus", @@ -7842,7 +7922,7 @@ dependencies = [ "aptosdb", "async-trait", "bcs", - "bytes", + "bytes 1.1.0", "channel", "claim", "consensus-notifications", @@ -7981,7 +8061,7 @@ dependencies = [ "aptos-types", "bcs", "bounded-executor", - "bytes", + "bytes 1.1.0", "channel", "claim", "futures", @@ -8425,7 +8505,7 @@ version = "1.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4903bf0427cf68dddd5aa6a93220756f8be0c34fcfa9f5e6191e103e15a31395" dependencies = [ - "bytes", + "bytes 1.1.0", "libc", "memchr", "mio 0.8.3", @@ -8487,9 +8567,20 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" dependencies = [ - "rustls", + "rustls 0.19.1", "tokio", - "webpki", + "webpki 0.21.4", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls 0.20.6", + "tokio", + "webpki 0.22.0", ] [[package]] @@ -8510,7 +8601,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53474327ae5e166530d17f2d956afcb4f8a004de581b3cae10f12006bc8163e3" dependencies = [ "async-stream", - "bytes", + "bytes 1.1.0", "futures-core", "tokio", "tokio-stream", @@ -8535,7 +8626,7 @@ version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" dependencies = [ - "bytes", + "bytes 1.1.0", "futures-core", "futures-sink", "log", @@ -8549,7 +8640,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f988a1a1adc2fb21f9c12aa96441da33a1728193ae0b95d2be22dbd17fcb4e5c" dependencies = [ - "bytes", + "bytes 1.1.0", "futures-core", "futures-io", "futures-sink", @@ -8771,9 +8862,9 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0b2d8558abd2e276b0a8df5c05a2ec762609344191e5fd23e292c910e9165b5" dependencies = [ - "base64", + "base64 0.13.0", "byteorder", - "bytes", + "bytes 1.1.0", "http", "httparse", "log", @@ -8809,6 +8900,19 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0685c84d5d54d1c26f7d3eb96cd41550adb97baed141a761cf335d3d33bcd0ae" +[[package]] +name = "typed-headers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3179a61e9eccceead5f1574fd173cf2e162ac42638b9bf214c6ad0baf7efa24a" +dependencies = [ + "base64 0.11.0", + "bytes 0.5.6", + "chrono", + "http", + "mime", +] + [[package]] name = "typenum" version = "1.15.0" @@ -8965,7 +9069,7 @@ version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b8b063c2d59218ae09f22b53c42eaad0d53516457905f5235ca4bc9e99daa71" dependencies = [ - "base64", + "base64 0.13.0", "chunked_transfer", "log", "native-tls", @@ -9145,7 +9249,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cef4e1e9114a4b7f1ac799f16ce71c14de5778500c5450ec6b7b920c55b587e" dependencies = [ - "bytes", + "bytes 1.1.0", "futures-channel", "futures-util", "headers", @@ -9162,7 +9266,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "tokio", - "tokio-rustls", + "tokio-rustls 0.22.0", "tokio-stream", "tokio-tungstenite", "tokio-util 0.6.10", @@ -9170,6 +9274,20 @@ dependencies = [ "tracing", ] +[[package]] +name = "warp-reverse-proxy" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21bb86f4c86d0fb386acf34fa5360ea0e8a082835e05296da2286c38a3316283" +dependencies = [ + "hyper", + "once_cell", + "reqwest", + "thiserror", + "unicase", + "warp", +] + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -9289,6 +9407,16 @@ dependencies = [ "untrusted", ] +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "which" version = "4.2.5" diff --git a/Cargo.toml b/Cargo.toml index b4f19e80ca7bf..134f1ff30a555 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ members = [ "crates/aptos-log-derive", "crates/aptos-logger", "crates/aptos-metrics-core", + "crates/aptos-openapi", "crates/aptos-proptest-helpers", "crates/aptos-rate-limiter", "crates/aptos-rest-client", diff --git a/api/Cargo.toml b/api/Cargo.toml index 9e6873033a7fd..4f09a10c5023d 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aptos-api" -version = "0.1.0" +version = "0.2.0" authors = ["Aptos Labs "] description = "Aptos REST API" repository = "https://github.com/aptos-labs/aptos-core" @@ -17,12 +17,17 @@ fail = "0.5.0" futures = "0.3.21" hex = "0.4.3" hyper = "0.14.18" +mime = "0.3.16" once_cell = "1.10.0" percent-encoding = "2.1.0" +poem = { version = "1.3.35", features = ["anyhow", "rustls"] } +poem-openapi = { version = "2.0.5", features = ["swagger-ui", "url"] } serde = { version = "1.0.137", features = ["derive"], default-features = false } serde_json = { version = "1.0.81", features = ["preserve_order"] } tokio = { version = "1.18.2", features = ["full"] } +url = "2.2.2" warp = { version = "0.3.2", features = ["default", "tls"] } +warp-reverse-proxy = "0.5.0" aptos-api-types = { path = "./types", package = "aptos-api-types" } aptos-config = { path = "../config" } diff --git a/api/src/accounts.rs b/api/src/accounts.rs index 1ca344ff07de5..03b3c0e271934 100644 --- a/api/src/accounts.rs +++ b/api/src/accounts.rs @@ -142,9 +142,9 @@ impl Account { .map_err(anyhow::Error::from)? .ok_or_else(|| self.resource_not_found(&AccountResource::struct_tag()))?; - let account: AccountData = account_resource.into(); + let account_data: AccountData = account_resource.into(); - Response::new(self.latest_ledger_info, &account) + Response::new(self.latest_ledger_info, &account_data) } pub fn resources(self) -> Result { diff --git a/api/src/context.rs b/api/src/context.rs index 37f8b06b62600..ea0cddc6e203f 100644 --- a/api/src/context.rs +++ b/api/src/context.rs @@ -16,21 +16,24 @@ use aptos_types::{ contract_event::ContractEvent, event::EventKey, ledger_info::LedgerInfoWithSignatures, - state_store::{state_key::StateKey, state_key_prefix::StateKeyPrefix}, + state_store::{state_key::StateKey, state_key_prefix::StateKeyPrefix, state_value::StateValue}, transaction::{SignedTransaction, TransactionWithProof, Version}, write_set::WriteOp, }; use aptos_vm::data_cache::{IntoMoveResolver, RemoteStorageOwned}; use futures::{channel::oneshot, SinkExt}; use move_deps::move_core_types::ident_str; +use poem_openapi::payload::Json; use serde::{Deserialize, Serialize}; -use std::{convert::Infallible, sync::Arc}; +use std::{collections::HashMap, convert::Infallible, sync::Arc}; use storage_interface::{ state_view::{DbStateView, DbStateViewAtVersion, LatestDbStateCheckpointView}, DbReader, Order, }; use warp::{filters::BoxedFilter, Filter, Reply}; +use crate::poem_backend::{AptosError, AptosErrorCode, AptosErrorResponse, AptosInternalResult}; + // Context holds application scope context #[derive(Clone)] pub struct Context { @@ -61,6 +64,18 @@ impl Context { .map(|state_view| state_view.into_move_resolver()) } + pub fn move_resolver_poem(&self) -> AptosInternalResult> { + self.move_resolver().map_err(|e| { + AptosErrorResponse::InternalServerError(Json( + AptosError::new( + format_err!("Failed to read latest state checkpoint from DB: {}", e) + .to_string(), + ) + .error_code(AptosErrorCode::ReadFromStorageError), + )) + }) + } + pub fn state_view_at_version(&self, version: Version) -> Result { self.db.state_view_at_version(Some(version)) } @@ -103,6 +118,15 @@ impl Context { } } + pub fn get_latest_ledger_info_poem(&self) -> AptosInternalResult { + self.get_latest_ledger_info().map_err(|e| { + AptosErrorResponse::InternalServerError(Json( + AptosError::new(format_err!("Failed to retrieve ledger info: {}", e).to_string()) + .error_code(AptosErrorCode::ReadFromStorageError), + )) + }) + } + pub fn get_latest_ledger_info_with_signatures(&self) -> Result { self.db.get_latest_ledger_info() } @@ -113,16 +137,34 @@ impl Context { .get_state_value(state_key) } + pub fn get_state_value_poem( + &self, + state_key: &StateKey, + version: u64, + ) -> Result>, AptosErrorResponse> { + self.get_state_value(state_key, version).map_err(|e| { + AptosErrorResponse::InternalServerError(Json( + AptosError::new(format_err!("Failed to retrieve state value: {}", e).to_string()) + .error_code(AptosErrorCode::ReadFromStorageError), + )) + }) + } + + pub fn get_state_values( + &self, + address: AccountAddress, + version: u64, + ) -> Result> { + self.db + .get_state_values_by_key_prefix(&StateKeyPrefix::from(address), version) + } + pub fn get_account_state( &self, address: AccountAddress, version: u64, ) -> Result> { - AccountState::from_access_paths_and_values( - &self - .db - .get_state_values_by_key_prefix(&StateKeyPrefix::from(address), version)?, - ) + AccountState::from_access_paths_and_values(&self.get_state_values(address, version)?) } pub fn get_block_timestamp(&self, version: u64) -> Result { @@ -198,7 +240,7 @@ impl Context { if path.address == CORE_CODE_ADDRESS && typ == block_metadata_type { if let WriteOp::Value(value) = op { if let Ok(mut resource) = converter.try_into_resource(&typ, value) { - if let Some(value) = resource.data.0.remove(height_id) { + if let Some(value) = resource.data.0.remove(&height_id.into()) { if let Ok(height) = serde_json::from_value::(value) { return Some(height.0); } diff --git a/api/src/failpoint.rs b/api/src/failpoint.rs index 0208827a53754..de38cc097f43d 100644 --- a/api/src/failpoint.rs +++ b/api/src/failpoint.rs @@ -1,10 +1,14 @@ // Copyright (c) Aptos // SPDX-License-Identifier: Apache-2.0 -#[allow(unused_imports)] +#![allow(unused_imports)] + use anyhow::{format_err, Result}; use aptos_api_types::Error; +use crate::poem_backend::{AptosError, AptosErrorResponse}; +use poem_openapi::payload::Json; + #[allow(unused_variables)] #[inline] pub fn fail_point(name: &str) -> Result<(), Error> { @@ -12,3 +16,13 @@ pub fn fail_point(name: &str) -> Result<(), Error> { Err(format_err!("unexpected internal error for {}", name).into()) })) } + +#[allow(unused_variables)] +#[inline] +pub fn fail_point_poem(name: &str) -> Result<(), AptosErrorResponse> { + Ok(fail::fail_point!(format!("api::{}", name).as_str(), |_| { + Err(AptosErrorResponse::InternalServerError(Json( + AptosError::new(format!("unexpected internal error for {}", name)), + ))) + })) +} diff --git a/api/src/lib.rs b/api/src/lib.rs index 5419b6e2c6695..05711df8bd167 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -11,6 +11,7 @@ pub mod log; pub mod metrics; mod page; pub mod param; +mod poem_backend; pub mod runtime; mod state; mod transactions; diff --git a/api/src/poem_backend/accept_type.rs b/api/src/poem_backend/accept_type.rs new file mode 100644 index 0000000000000..98e987a0cd79c --- /dev/null +++ b/api/src/poem_backend/accept_type.rs @@ -0,0 +1,38 @@ +// Copyright (c) Aptos +// SPDX-License-Identifier: Apache-2.0 + +use std::convert::TryFrom; + +use poem::web::Accept; +use poem_openapi::payload::Json; + +use super::{AptosError, AptosErrorCode, AptosErrorResponse}; + +#[derive(PartialEq)] +pub enum AcceptType { + Json, + Bcs, +} + +impl TryFrom<&Accept> for AcceptType { + type Error = AptosErrorResponse; + + fn try_from(accept: &Accept) -> Result { + for mime in &accept.0 { + match mime.as_ref() { + "application/json" => return Ok(AcceptType::Json), + "application/x-bcs" => return Ok(AcceptType::Bcs), + "*/*" => {} + wildcard => { + return Err(AptosErrorResponse::BadRequest(Json( + AptosError::new(format!("Invalid Accept type: {:?}", wildcard)) + .error_code(AptosErrorCode::UnsupportedAcceptType), + ))); + } + } + } + + // Default to returning content as JSON. + Ok(AcceptType::Json) + } +} diff --git a/api/src/poem_backend/accounts.rs b/api/src/poem_backend/accounts.rs new file mode 100644 index 0000000000000..bd7ae98f0365e --- /dev/null +++ b/api/src/poem_backend/accounts.rs @@ -0,0 +1,363 @@ +// Copyright (c) Aptos +// SPDX-License-Identifier: Apache-2.0 + +use std::convert::{TryFrom, TryInto}; +use std::sync::Arc; + +use super::accept_type::AcceptType; +use super::response::deserialize_from_bcs; +use super::{ + response::{AptosInternalResult, AptosResponseResult}, + ApiTags, AptosResponse, +}; +use super::{AptosError, AptosErrorCode, AptosErrorResponse}; +use crate::context::Context; +use crate::failpoint::fail_point_poem; +use anyhow::format_err; +use aptos_api_types::{AccountData, Address, AsConverter, MoveStructTag, TransactionId}; +use aptos_api_types::{LedgerInfo, MoveModuleBytecode, MoveResource}; +use aptos_types::access_path::AccessPath; +use aptos_types::account_config::AccountResource; +use aptos_types::account_state::AccountState; +use aptos_types::event::EventHandle; +use aptos_types::event::EventKey; +use aptos_types::state_store::state_key::StateKey; +use move_deps::move_core_types::value::MoveValue; +use move_deps::move_core_types::{ + identifier::Identifier, + language_storage::{ResourceKey, StructTag}, + move_resource::MoveStructType, +}; +use poem::web::Accept; +use poem_openapi::param::Query; +use poem_openapi::payload::Json; +use poem_openapi::{param::Path, OpenApi}; + +// TODO: Make a helper that builds an AptosResponse from just an anyhow error, +// that assumes that it's an internal error. We can use .context() add more info. + +pub struct AccountsApi { + pub context: Arc, +} + +#[OpenApi] +impl AccountsApi { + /// Get account + /// + /// Return high level information about an account such as its sequence number. + #[oai( + path = "/accounts/:address", + method = "get", + operation_id = "get_account", + tag = "ApiTags::General" + )] + async fn get_account( + &self, + accept: Accept, + address: Path
, + ledger_version: Query>, + ) -> AptosResponseResult { + fail_point_poem("endpoint_get_account")?; + let accept_type = AcceptType::try_from(&accept)?; + let account = Account::new(self.context.clone(), address.0, ledger_version.0)?; + account.account(&accept_type) + } + + /// Get account resources + /// + /// This API returns account resources for a specific ledger version (AKA transaction version). + /// If not present, the latest version is used. <---- TODO Update this comment + /// The Aptos nodes prune account state history, via a configurable time window (link). + /// If the requested data has been pruned, the server responds with a 404 + #[oai( + path = "/accounts/:address/resources", + method = "get", + operation_id = "get_account_resources", + tag = "ApiTags::General" + )] + async fn get_account_resources( + &self, + accept: Accept, + address: Path
, + ledger_version: Query>, + ) -> AptosResponseResult> { + fail_point_poem("endpoint_get_account_resources")?; + let accept_type = AcceptType::try_from(&accept)?; + let account = Account::new(self.context.clone(), address.0, ledger_version.0)?; + account.resources(&accept_type) + } + + /// Get account modules + /// + /// This API returns account resources for a specific ledger version (AKA transaction version). + /// If not present, the latest version is used. <---- TODO Update this comment + /// The Aptos nodes prune account state history, via a configurable time window (link). + /// If the requested data has been pruned, the server responds with a 404 + #[oai( + path = "/accounts/:address/modules", + method = "get", + operation_id = "get_account_modules", + tag = "ApiTags::General" + )] + async fn get_account_modules( + &self, + accept: Accept, + address: Path
, + ledger_version: Query>, + ) -> AptosResponseResult> { + fail_point_poem("endpoint_get_account_resources")?; + let accept_type = AcceptType::try_from(&accept)?; + let account = Account::new(self.context.clone(), address.0, ledger_version.0)?; + account.modules(&accept_type) + } +} + +pub struct Account { + context: Arc, + address: Address, + ledger_version: u64, + latest_ledger_info: LedgerInfo, +} + +impl Account { + pub fn new( + context: Arc, + address: Address, + requested_ledger_version: Option, + ) -> Result { + let latest_ledger_info = context.get_latest_ledger_info_poem()?; + let ledger_version: u64 = + requested_ledger_version.unwrap_or_else(|| latest_ledger_info.version()); + + if ledger_version > latest_ledger_info.version() { + return Err(AptosErrorResponse::not_found( + "ledger", + TransactionId::Version(ledger_version), + latest_ledger_info.version(), + )); + } + + Ok(Self { + context, + address, + ledger_version, + latest_ledger_info, + }) + } + + // These functions map directly to endpoint functions. + + pub fn account(self, accept_type: &AcceptType) -> AptosResponseResult { + let state_key = StateKey::AccessPath(AccessPath::resource_access_path(ResourceKey::new( + self.address.into(), + AccountResource::struct_tag(), + ))); + + let state_value = self + .context + .get_state_value_poem(&state_key, self.ledger_version)?; + + let state_value = match state_value { + Some(state_value) => state_value, + None => return Err(self.resource_not_found(&AccountResource::struct_tag())), + }; + + let account_resource = deserialize_from_bcs::(&state_value)?; + let account_data: AccountData = account_resource.into(); + + AptosResponse::try_from_rust_value(account_data, &self.latest_ledger_info, accept_type) + } + + pub fn resources(self, accept_type: &AcceptType) -> AptosResponseResult> { + let account_state = self.account_state()?; + let resources = account_state.get_resources(); + let move_resolver = self.context.move_resolver_poem()?; + let converted_resources = move_resolver + .as_converter() + .try_into_resources(resources) + .map_err(|e| { + AptosErrorResponse::InternalServerError(Json( + AptosError::new( + format_err!( + "Failed to build move resource response from data in DB: {}", + e + ) + .to_string(), + ) + .error_code(AptosErrorCode::InvalidBcsInStorageError), + )) + })?; + AptosResponse::>::try_from_rust_value( + converted_resources, + &self.latest_ledger_info, + accept_type, + ) + } + + pub fn modules(self, accept_type: &AcceptType) -> AptosResponseResult> { + let mut modules = Vec::new(); + for module in self.account_state()?.into_modules() { + modules.push( + MoveModuleBytecode::new(module) + .try_parse_abi() + .map_err(|e| { + AptosErrorResponse::InternalServerError(Json( + AptosError::new( + format_err!("Failed to parse move module ABI: {}", e).to_string(), + ) + .error_code(AptosErrorCode::InvalidBcsInStorageError), + )) + })?, + ); + } + AptosResponse::>::try_from_rust_value( + modules, + &self.latest_ledger_info, + accept_type, + ) + } + + // Helpers for processing account state. + + fn account_state(&self) -> AptosInternalResult { + let state = self + .context + .get_account_state(self.address.into(), self.ledger_version) + .map_err(|e| { + AptosErrorResponse::InternalServerError(Json( + AptosError::new( + format_err!("Failed to read account state from DB: {}", e).to_string(), + ) + .error_code(AptosErrorCode::ReadFromStorageError), + )) + })? + .ok_or_else(|| self.account_not_found())?; + Ok(state) + } + + // Helpers for building errors. + + fn account_not_found(&self) -> AptosErrorResponse { + AptosErrorResponse::not_found( + "account", + format!( + "address({}) and ledger version({})", + self.address, self.ledger_version + ), + self.latest_ledger_info.version(), + ) + } + + fn resource_not_found(&self, struct_tag: &StructTag) -> AptosErrorResponse { + AptosErrorResponse::not_found( + "resource", + format!( + "address({}), struct tag({}) and ledger version({})", + self.address, struct_tag, self.ledger_version + ), + self.latest_ledger_info.version(), + ) + } + + fn field_not_found( + &self, + struct_tag: &StructTag, + field_name: &Identifier, + ) -> AptosErrorResponse { + AptosErrorResponse::not_found( + "resource", + format!( + "address({}), struct tag({}), field name({}) and ledger version({})", + self.address, struct_tag, field_name, self.ledger_version + ), + self.latest_ledger_info.version(), + ) + } + + // TODO: Break this up into 3 structs / traits. There is common stuff, + // account specific stuff, and event specific stuff. + + // Events specific stuff. + + pub fn find_event_key( + &self, + event_handle: MoveStructTag, + field_name: Identifier, + ) -> AptosInternalResult { + let struct_tag: StructTag = event_handle.try_into().map_err(|e| { + AptosErrorResponse::BadRequest(Json(AptosError::new( + format_err!("Given event handle was invalid: {}", e).to_string(), + ))) + })?; + + let resource = self.find_resource(&struct_tag)?; + + let (_id, value) = resource + .into_iter() + .find(|(id, _)| id == &field_name) + .ok_or_else(|| self.field_not_found(&struct_tag, &field_name))?; + + // serialization should not fail, otherwise it's internal bug + // TODO: don't unwrap + let event_handle_bytes = bcs::to_bytes(&value).unwrap(); + // deserialization may fail because the bytes are not EventHandle struct type. + let event_handle: EventHandle = bcs::from_bytes(&event_handle_bytes).map_err(|e| { + AptosErrorResponse::BadRequest(Json(AptosError::new( + format_err!( + "Deserialization error, field({}) type is not EventHandle struct: {}", + field_name, + e + ) + .to_string(), + ))) + })?; + Ok(*event_handle.key()) + } + + fn find_resource( + &self, + struct_tag: &StructTag, + ) -> AptosInternalResult> { + let account_state = self.account_state()?; + let (typ, data) = account_state + .get_resources() + .find(|(tag, _data)| tag == struct_tag) + .ok_or_else(|| self.resource_not_found(struct_tag))?; + let move_resolver = self.context.move_resolver_poem()?; + move_resolver + .as_converter() + .move_struct_fields(&typ, data) + .map_err(|e| { + AptosErrorResponse::InternalServerError(Json( + AptosError::new( + format_err!("Failed to convert move structs: {}", e).to_string(), + ) + .error_code(AptosErrorCode::ReadFromStorageError), + )) + }) + } +} + +// TODO: For the BCS response type, instead of constructing the Rust type from +// BCS just to serialize it back again, return the bytes directly. This is an +// example of doing that, but it requires extensive testing: +/* + let state_values = self + .context + .get_state_values(self.address.into(), self.ledger_version) + .ok_or_else(|| self.account_not_found())?; + match accept_type { + AcceptType::Bcs => Ok(AptosResponse::from_bcs( + state_value, + &self.latest_ledger_info, + )), + AcceptType::Json => { + let account_resource = deserialize_from_bcs::(&state_value)?; + let account_data: AccountData = account_resource.into(); + Ok(AptosResponse::from_json( + account_data, + &self.latest_ledger_info, + )) + } + } +*/ diff --git a/api/src/poem_backend/basic.rs b/api/src/poem_backend/basic.rs new file mode 100644 index 0000000000000..f270fad126a7a --- /dev/null +++ b/api/src/poem_backend/basic.rs @@ -0,0 +1,33 @@ +// Copyright (c) Aptos +// SPDX-License-Identifier: Apache-2.0 + +use std::sync::Arc; + +use super::ApiTags; +use crate::context::Context; +use poem_openapi::{payload::Html, OpenApi}; + +const OPEN_API_HTML: &str = include_str!("../../doc/spec.html"); + +pub struct BasicApi { + pub context: Arc, +} + +// TODO: Consider using swagger UI here instead since it's built in, though +// the UI is much worse. I could look into adding the Elements UI to Poem. + +#[OpenApi] +impl BasicApi { + /// Show OpenAPI explorer + /// + /// Provides a UI that you can use to explore the API. You can also retrieve the API directly at `/openapi.yaml` and `/openapi.json`. + #[oai( + path = "/spec", + method = "get", + operation_id = "openapi", + tag = "ApiTags::General" + )] + async fn openapi(&self) -> Html { + Html(OPEN_API_HTML.to_string()) + } +} diff --git a/api/src/poem_backend/bcs_payload.rs b/api/src/poem_backend/bcs_payload.rs new file mode 100644 index 0000000000000..be626108b0112 --- /dev/null +++ b/api/src/poem_backend/bcs_payload.rs @@ -0,0 +1,109 @@ +// Copyright (c) Aptos +// SPDX-License-Identifier: Apache-2.0 + +/// This module defines a Poem payload type for BCS. JSON is already natively +/// supported. This payload type is as permissive as possible, as long as it +/// can be (de)serialized with serde, meaning it can be used with BCS, it can +/// be used with this payload type. For the most part this is just following +/// the custom payload example in the Poem repo. +use std::ops::{Deref, DerefMut}; + +use bcs::{from_bytes, to_bytes}; +use poem::{ + http::{header, StatusCode}, + FromRequest, IntoResponse, Request, RequestBody, Response, Result, +}; +use poem_openapi::{ + error::ParseRequestPayloadError, + impl_apirequest_for_payload, + payload::{ParsePayload, Payload}, + registry::{MetaMediaType, MetaResponse, MetaResponses, MetaSchemaRef, Registry}, + types::Type, + ApiResponse, +}; +use serde::{Deserialize, Serialize}; + +pub const CONTENT_TYPE: &str = "application/x-bcs"; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Bcs(pub T) +where + T: Type; + +impl Deref for Bcs { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Bcs { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Payload for Bcs { + const CONTENT_TYPE: &'static str = CONTENT_TYPE; + + fn schema_ref() -> MetaSchemaRef { + T::schema_ref() + } + + #[allow(unused_variables)] + fn register(registry: &mut Registry) { + T::register(registry); + } +} + +#[poem::async_trait] +impl Deserialize<'b>> ParsePayload for Bcs { + const IS_REQUIRED: bool = true; + + async fn from_request(request: &Request, body: &mut RequestBody) -> Result { + let data: Vec = FromRequest::from_request(request, body).await?; + let value: T = from_bytes(&data).map_err(|err| ParseRequestPayloadError { + reason: err.to_string(), + })?; + Ok(Self(value)) + } +} + +impl IntoResponse for Bcs { + fn into_response(self) -> Response { + let data = match to_bytes(&self.0) { + Ok(data) => data, + Err(err) => { + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(err.to_string()) + } + }; + Response::builder() + .header(header::CONTENT_TYPE, Self::CONTENT_TYPE) + .body(data) + } +} + +impl ApiResponse for Bcs { + fn meta() -> MetaResponses { + MetaResponses { + responses: vec![MetaResponse { + description: "BCS: Binary Canonical Serialization", + status: Some(200), + content: vec![MetaMediaType { + content_type: Self::CONTENT_TYPE, + schema: Self::schema_ref(), + }], + headers: vec![], + }], + } + } + + fn register(registry: &mut Registry) { + T::register(registry); + } +} + +impl_apirequest_for_payload!(Bcs, T: Type + for<'b> Deserialize<'b>); diff --git a/api/src/poem_backend/events.rs b/api/src/poem_backend/events.rs new file mode 100644 index 0000000000000..af954b4ee83e7 --- /dev/null +++ b/api/src/poem_backend/events.rs @@ -0,0 +1,131 @@ +// Copyright (c) Aptos +// SPDX-License-Identifier: Apache-2.0 + +use std::convert::TryFrom; +use std::sync::Arc; + +use super::accept_type::AcceptType; +use super::accounts::Account; +use super::page::Page; +use super::{response::AptosResponseResult, ApiTags, AptosResponse}; +use super::{AptosError, AptosErrorCode, AptosErrorResponse}; +use crate::context::Context; +use crate::failpoint::fail_point_poem; +use anyhow::format_err; +use aptos_api_types::{Address, EventKey, IdentifierWrapper, MoveStructTagWrapper}; +use aptos_api_types::{AsConverter, Event}; +use poem::web::Accept; +use poem_openapi::param::Query; +use poem_openapi::payload::Json; +use poem_openapi::{param::Path, OpenApi}; + +// TODO: Make a helper that builds an AptosResponse from just an anyhow error, +// that assumes that it's an internal error. We can use .context() add more info. + +pub struct EventsApi { + pub context: Arc, +} + +#[OpenApi] +impl EventsApi { + /// Get events by event key + /// + /// todo + #[oai( + path = "/events/:event_key", + method = "get", + operation_id = "get_events_by_event_key", + tag = "ApiTags::General" + )] + async fn get_events_by_event_key( + &self, + accept: Accept, + // TODO: Make this a little smarter, in the spec this just looks like a string. + // Consider unpacking the inner EventKey type and taking two params, the creation + // number and the address. + event_key: Path, + start: Query>, + limit: Query>, + ) -> AptosResponseResult> { + fail_point_poem("endpoint_get_events_by_event_key")?; + let accept_type = AcceptType::try_from(&accept)?; + let page = Page::new(start.0, limit.0); + self.list(&accept_type, page, event_key.0) + } + + /// Get events by event handle + /// + /// This API extracts event key from the account resource identified + /// by the `event_handle_struct` and `field_name`, then returns + /// events identified by the event key. + #[oai( + path = "/accounts/:address/events/:event_handle/:field_name", + method = "get", + operation_id = "get_events_by_event_handle", + tag = "ApiTags::General" + )] + async fn get_events_by_event_handle( + &self, + accept: Accept, + address: Path
, + event_handle: Path, + field_name: Path, + start: Query>, + limit: Query>, + ) -> AptosResponseResult> { + fail_point_poem("endpoint_get_events_by_event_handle")?; + let accept_type = AcceptType::try_from(&accept)?; + let page = Page::new(start.0, limit.0); + let account = Account::new(self.context.clone(), address.0, None)?; + let key = account + .find_event_key(event_handle.0.into(), field_name.0.into())? + .into(); + self.list(&accept_type, page, key) + } +} + +impl EventsApi { + fn list( + &self, + accept_type: &AcceptType, + page: Page, + event_key: EventKey, + ) -> AptosResponseResult> { + let latest_ledger_info = self.context.get_latest_ledger_info_poem()?; + let contract_events = self + .context + .get_events( + &event_key.into(), + page.start(0, u64::MAX)?, + page.limit()?, + latest_ledger_info.version(), + ) + // TODO: Previously this was a 500, but I'm making this a 400. I suspect + // both could be true depending on the error. Make this more specific. + .map_err(|e| { + AptosErrorResponse::BadRequest(Json( + AptosError::new( + format_err!("Failed to find events by key {}: {}", event_key, e) + .to_string(), + ) + .error_code(AptosErrorCode::InvalidBcsInStorageError), + )) + })?; + + let resolver = self.context.move_resolver_poem()?; + let events = resolver + .as_converter() + .try_into_events(&contract_events) + .map_err(|e| { + AptosErrorResponse::InternalServerError(Json( + AptosError::new( + format_err!("Failed to convert events from storage into response: {}", e) + .to_string(), + ) + .error_code(AptosErrorCode::InvalidBcsInStorageError), + )) + })?; + + AptosResponse::try_from_rust_value(events, &latest_ledger_info, accept_type) + } +} diff --git a/api/src/poem_backend/index.rs b/api/src/poem_backend/index.rs new file mode 100644 index 0000000000000..8c0cf0a54f71f --- /dev/null +++ b/api/src/poem_backend/index.rs @@ -0,0 +1,35 @@ +// Copyright (c) Aptos +// SPDX-License-Identifier: Apache-2.0 + +use std::{convert::TryFrom, sync::Arc}; + +use super::accept_type::AcceptType; +use super::{response::AptosResponseResult, ApiTags, AptosResponse}; +use crate::context::Context; +use aptos_api_types::IndexResponse; +use poem::web::Accept; +use poem_openapi::OpenApi; + +pub struct IndexApi { + pub context: Arc, +} + +#[OpenApi] +impl IndexApi { + /// Get ledger info + /// + /// Get the latest ledger information, including data such as chain ID, role type, ledger versions, epoch, etc. + #[oai( + path = "/", + method = "get", + operation_id = "get_ledger_info", + tag = "ApiTags::General" + )] + async fn get_ledger_info(&self, accept: Accept) -> AptosResponseResult { + let accept_type = AcceptType::try_from(&accept)?; + let ledger_info = self.context.get_latest_ledger_info_poem()?; + let node_role = self.context.node_role(); + let index_response = IndexResponse::new(ledger_info.clone(), node_role); + AptosResponse::try_from_rust_value(index_response, &ledger_info, &accept_type) + } +} diff --git a/api/src/poem_backend/log.rs b/api/src/poem_backend/log.rs new file mode 100644 index 0000000000000..989d5572233fa --- /dev/null +++ b/api/src/poem_backend/log.rs @@ -0,0 +1,75 @@ +// Copyright (c) Aptos +// SPDX-License-Identifier: Apache-2.0 + +use std::time::Duration; + +use crate::metrics::RESPONSE_STATUS; +use aptos_logger::{ + debug, error, + prelude::{sample, SampleRate}, + sample::Sampling, + Schema, +}; +use poem::{http::header, Endpoint, Request, Response, Result}; + +/// Logs information about the request and response if the response status code +/// is >= 500, to help us debug since this will be an error on our side. +/// We also do general logging of the status code alone regardless of what it is. +pub async fn middleware_log(next: E, request: Request) -> Result { + let start = std::time::Instant::now(); + + let mut log = HttpRequestLog { + remote_addr: request.remote_addr().as_socket_addr().cloned(), + method: request.method().to_string(), + path: request.uri().path().to_string(), + status: 0, + referer: request + .headers() + .get(header::REFERER) + .and_then(|v| v.to_str().ok().map(|v| v.to_string())), + user_agent: request + .headers() + .get(header::USER_AGENT) + .and_then(|v| v.to_str().ok().map(|v| v.to_string())), + elapsed: Duration::from_secs(0), + forwarded: request + .headers() + .get(header::FORWARDED) + .and_then(|v| v.to_str().ok().map(|v| v.to_string())), + }; + + let response = next.get_response(request).await; + + let elapsed = start.elapsed(); + + log.status = response.status().as_u16(); + log.elapsed = elapsed; + + if log.status >= 500 { + sample!(SampleRate::Duration(Duration::from_secs(1)), error!(log)); + } else { + debug!(log); + } + + RESPONSE_STATUS + .with_label_values(&[log.status.to_string().as_str()]) + .observe(elapsed.as_secs_f64()); + + Ok(response) +} + +// TODO: Figure out how to have certain fields be borrowed, like in the +// original implementation. +#[derive(Schema)] +pub struct HttpRequestLog { + #[schema(display)] + remote_addr: Option, + method: String, + path: String, + pub status: u16, + referer: Option, + user_agent: Option, + #[schema(debug)] + pub elapsed: std::time::Duration, + forwarded: Option, +} diff --git a/api/src/poem_backend/mod.rs b/api/src/poem_backend/mod.rs new file mode 100644 index 0000000000000..0af6b33227df5 --- /dev/null +++ b/api/src/poem_backend/mod.rs @@ -0,0 +1,41 @@ +// Copyright (c) Aptos +// SPDX-License-Identifier: Apache-2.0 + +use poem_openapi::Tags; + +mod accept_type; +mod accounts; +mod basic; +mod bcs_payload; +mod events; +mod index; +mod log; +mod page; +mod post; +mod response; +mod runtime; +mod transactions; + +#[derive(Tags)] +pub enum ApiTags { + /// General information. + General, +} + +pub use accounts::AccountsApi; +pub use basic::BasicApi; +pub use events::EventsApi; +pub use index::IndexApi; +pub use log::middleware_log; +pub use post::AptosPost; +pub use response::{ + AptosError, AptosErrorCode, AptosErrorResponse, AptosInternalResult, AptosResponse, + AptosResponseContent, +}; +pub use runtime::attach_poem_to_runtime; +pub use transactions::TransactionsApi; + +// TODO: Move these impls throughout each of the files in the parent directory. +// The only reason I do it here right now is the existing handler functions return +// opaque reply objects and therefore I can't re-use them, so I'd have to pollute +// those files with these impls below. diff --git a/api/src/poem_backend/page.rs b/api/src/poem_backend/page.rs new file mode 100644 index 0000000000000..bb5a5eb25ad77 --- /dev/null +++ b/api/src/poem_backend/page.rs @@ -0,0 +1,61 @@ +// Copyright (c) Aptos +// SPDX-License-Identifier: Apache-2.0 + +use super::{response::AptosInternalResult, AptosError, AptosErrorCode, AptosErrorResponse}; +use poem_openapi::payload::Json; +use serde::Deserialize; + +const DEFAULT_PAGE_SIZE: u16 = 25; +const MAX_PAGE_SIZE: u16 = 1000; + +#[derive(Clone, Debug, Deserialize)] +pub(crate) struct Page { + start: Option, + limit: Option, +} + +impl Page { + pub fn new(start: Option, limit: Option) -> Self { + Self { start, limit } + } +} + +impl Page { + pub fn start(&self, default: u64, max: u64) -> AptosInternalResult { + let start = self.start.unwrap_or(default); + if start > max { + return Err(AptosErrorResponse::BadRequest(Json( + AptosError::new( + anyhow::format_err!( + "Given start value ({}) is too large, it must be < {}", + start, + max + ) + .to_string(), + ) + .error_code(AptosErrorCode::InvalidBcsInStorageError), + ))); + } + Ok(start) + } + + pub fn limit(&self) -> AptosInternalResult { + let limit = self.limit.unwrap_or(DEFAULT_PAGE_SIZE); + if limit == 0 { + return Err(AptosErrorResponse::BadRequest(Json(AptosError::new( + anyhow::format_err!("Given limit value ({}) must not be zero", limit,).to_string(), + )))); + } + if limit > MAX_PAGE_SIZE { + return Err(AptosErrorResponse::BadRequest(Json(AptosError::new( + anyhow::format_err!( + "Given limit value ({}) is too large, it must be < {}", + limit, + MAX_PAGE_SIZE + ) + .to_string(), + )))); + } + Ok(limit) + } +} diff --git a/api/src/poem_backend/post.rs b/api/src/poem_backend/post.rs new file mode 100644 index 0000000000000..bd9744b0675a9 --- /dev/null +++ b/api/src/poem_backend/post.rs @@ -0,0 +1,20 @@ +// Copyright (c) Aptos +// SPDX-License-Identifier: Apache-2.0 + +use poem_openapi::{ + payload::Json, + types::{ParseFromJSON, ToJSON, Type}, + ApiRequest, +}; +use serde::Deserialize; + +use super::bcs_payload::Bcs; + +#[derive(ApiRequest)] +pub enum AptosPost Deserialize<'b>> { + #[oai(content_type = "application/x-bcs")] + Bcs(Bcs), + + #[oai(content_type = "application/json")] + Json(Json), +} diff --git a/api/src/poem_backend/response.rs b/api/src/poem_backend/response.rs new file mode 100644 index 0000000000000..8ebb0a81fdaf2 --- /dev/null +++ b/api/src/poem_backend/response.rs @@ -0,0 +1,199 @@ +// Copyright (c) Aptos +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::Display; + +use super::accept_type::AcceptType; +use anyhow::format_err; +use aptos_api_types::{LedgerInfo, U64}; +use poem::Result as PoemResult; +use poem_openapi::{payload::Json, types::ToJSON, ApiResponse, Enum, Object, ResponseContent}; +use serde::{Deserialize, Serialize}; + +use super::bcs_payload::Bcs; + +// This should be used for endpoints, signalling that they return either a +// response capturing success or failure. +pub type AptosResponseResult = PoemResult, AptosErrorResponse>; + +// This should be used for internal functions that need to return just a T +// but could fail, in which case we bubble an error response up to the client. +pub type AptosInternalResult = anyhow::Result; + +// TODO: Consdider having more specific error structs for different endpoints. +/// This is the generic struct we use for all API errors, it contains a string +/// message and an Aptos API specific error code. +#[derive(Debug, Object)] +pub struct AptosError { + message: String, + error_code: Option, + aptos_ledger_version: Option, +} + +impl AptosError { + pub fn new(message: String) -> Self { + Self { + message, + error_code: None, + aptos_ledger_version: None, + } + } + pub fn error_code(mut self, error_code: AptosErrorCode) -> Self { + self.error_code = Some(error_code); + self + } + + pub fn aptos_ledger_version(mut self, ledger_version: u64) -> Self { + self.aptos_ledger_version = Some(ledger_version.into()); + self + } +} + +/// These codes provide more granular error information beyond just the HTTP +/// status code of the response. +// Make sure the integer codes increment one by one. +#[derive(Debug, Enum)] +pub enum AptosErrorCode { + /// The Accept header contained an unsupported Accept type. + UnsupportedAcceptType = 0, + + /// The API failed to read from storage for this request, not because of a + /// bad request, but because of some internal error. + ReadFromStorageError = 1, + + /// The data we read from the DB was not valid BCS. + InvalidBcsInStorageError = 2, + + /// We were unexpectedly unable to convert a Rust type to BCS. + BcsSerializationError = 3, +} + +// TODO: Find a less repetitive way to do this. +#[derive(ApiResponse)] +pub enum AptosErrorResponse { + #[oai(status = 400)] + BadRequest(Json), + + #[oai(status = 404)] + NotFound(Json), + + #[oai(status = 500)] + InternalServerError(Json), +} + +#[derive(ResponseContent)] +pub enum AptosResponseContent { + // When returning data as JSON, we take in T and then serialize to JSON + // as part of the response. + Json(Json), + + // When returning data as BCS, we never actually interact with the Rust + // type. Instead, we just return the bytes we read from the DB directly, + // for efficiency reasons. Only through the `schema` decalaration at the + // endpoints does the return type make its way into the OpenAPI spec. + #[oai(actual_type = "Bcs")] + Bcs(Bcs>), +} + +#[derive(ApiResponse)] +pub enum AptosResponse { + #[oai(status = 200)] + Ok( + AptosResponseContent, + #[oai(header = "X-Aptos-Chain-Id")] u16, + #[oai(header = "X-Aptos-Ledger-Version")] u64, + #[oai(header = "X-Aptos-Ledger-Oldest-Version")] u64, + #[oai(header = "X-Aptos-Ledger-TimestampUsec")] u64, + #[oai(header = "X-Aptos-Epoch")] u64, + ), +} + +// From impls + +impl From for AptosError { + fn from(error: anyhow::Error) -> Self { + AptosError::new(error.to_string()) + } +} + +impl AptosErrorResponse { + pub fn not_found(resource: &str, identifier: S, ledger_version: u64) -> Self { + Self::NotFound(Json( + AptosError::new(format!("{} not found by {}", resource, identifier)) + .aptos_ledger_version(ledger_version), + )) + } + + pub fn invalid_param(name: &str, value: S) -> Self { + Self::BadRequest(Json(AptosError::new(format!( + "invalid parameter {}: {}", + name, value + )))) + } +} + +impl AptosResponse { + fn from_ledger_info(content: AptosResponseContent, ledger_info: &LedgerInfo) -> Self { + AptosResponse::Ok( + content, + ledger_info.chain_id as u16, + ledger_info.ledger_version.into(), + ledger_info.oldest_ledger_version.into(), + ledger_info.ledger_timestamp.into(), + ledger_info.epoch, + ) + } + + /// Construct a response from bytes that you know ahead of time a BCS + /// encoded value. + pub fn from_bcs(value: Vec, ledger_info: &LedgerInfo) -> Self { + Self::from_ledger_info(AptosResponseContent::Bcs(Bcs(value)), ledger_info) + } + + /// Construct an Aptos response from a Rust type, serializing it to JSON. + pub fn from_json(value: T, ledger_info: &LedgerInfo) -> Self { + Self::from_ledger_info(AptosResponseContent::Json(Json(value)), ledger_info) + } + + /// This is a convenience function for creating a response when you have + /// a Rust object from the beginning. If you're starting out with bytes, + /// you should instead check the accept type and use either `from_bcs` + /// or `from_json`. + pub fn try_from_rust_value( + value: T, + ledger_info: &LedgerInfo, + accept_type: &AcceptType, + ) -> Result { + match accept_type { + AcceptType::Bcs => Ok(AptosResponse::from_bcs( + serialize_to_bcs(&value)?, + ledger_info, + )), + AcceptType::Json => Ok(AptosResponse::from_json(value, ledger_info)), + } + } +} + +/// Serialize an internal Rust type to BCS, returning a 500 if it fails. +pub fn serialize_to_bcs(value: T) -> Result, AptosErrorResponse> { + bcs::to_bytes(&value).map_err(|e| { + AptosErrorResponse::InternalServerError(Json( + AptosError::new( + format_err!("Rust type could not be serialized to BCS: {}", e).to_string(), + ) + .error_code(AptosErrorCode::BcsSerializationError), + )) + }) +} + +/// Deserialize BCS bytes into an internal Rust, returning a 500 if it fails. +pub fn deserialize_from_bcs Deserialize<'b>>( + bytes: &[u8], +) -> Result { + bcs::from_bytes(bytes).map_err(|e| { + AptosErrorResponse::InternalServerError(Json( + AptosError::new(format_err!("Data in storage was not valid BCS: {}", e).to_string()) + .error_code(AptosErrorCode::InvalidBcsInStorageError), + )) + }) +} diff --git a/api/src/poem_backend/runtime.rs b/api/src/poem_backend/runtime.rs new file mode 100644 index 0000000000000..c95262dfc687c --- /dev/null +++ b/api/src/poem_backend/runtime.rs @@ -0,0 +1,120 @@ +// Copyright (c) Aptos +// SPDX-License-Identifier: Apache-2.0 + +use std::{net::SocketAddr, sync::Arc}; + +use super::{middleware_log, AccountsApi, BasicApi, EventsApi, IndexApi}; + +use crate::{context::Context, poem_backend::TransactionsApi}; +use anyhow::Context as AnyhowContext; +use aptos_config::config::NodeConfig; +use aptos_logger::info; +use poem::{ + http::{header, Method}, + listener::{Listener, RustlsCertificate, RustlsConfig, TcpListener}, + middleware::Cors, + EndpointExt, Route, Server, +}; +use poem_openapi::{ContactObject, LicenseObject, OpenApiService}; +use tokio::runtime::Runtime; + +/// Returns address it is running at. +pub fn attach_poem_to_runtime( + runtime: &Runtime, + context: Context, + config: &NodeConfig, +) -> anyhow::Result { + let context = Arc::new(context); + + let apis = ( + AccountsApi { + context: context.clone(), + }, + BasicApi { + context: context.clone(), + }, + EventsApi { + context: context.clone(), + }, + IndexApi { + context: context.clone(), + }, + TransactionsApi { context }, + ); + + let version = std::env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "0.1.0".to_string()); + let license = + LicenseObject::new("Apache 2.0").url("https://www.apache.org/licenses/LICENSE-2.0.html"); + let contact = ContactObject::new() + .name("Aptos Labs") + .url("https://github.com/aptos-labs/aptos-core"); + + // These APIs get merged. + let api_service = OpenApiService::new(apis, "Aptos Node API", version) + .description("The Aptos Node API is a RESTful API for client applications to interact with the Aptos blockchain.") + .license(license) + .contact(contact) + .external_document("https://github.com/aptos-labs/aptos-core"); + + let spec_json = api_service.spec_endpoint(); + let spec_yaml = api_service.spec_endpoint_yaml(); + + let mut address = config.api.address; + + // TODO: This is temporary while we serve both APIs simulatenously. + // Doing this means the OS assigns it an unused port. + address.set_port(0); + + let listener = match (&config.api.tls_cert_path, &config.api.tls_key_path) { + (Some(tls_cert_path), Some(tls_key_path)) => { + info!("Using TLS for API"); + let cert = std::fs::read_to_string(tls_cert_path).context(format!( + "Failed to read TLS cert from path: {}", + tls_cert_path + ))?; + let key = std::fs::read_to_string(tls_key_path).context(format!( + "Failed to read TLS key from path: {}", + tls_key_path + ))?; + let rustls_certificate = RustlsCertificate::new().cert(cert).key(key); + let rustls_config = RustlsConfig::new().fallback(rustls_certificate); + TcpListener::bind(address).rustls(rustls_config).boxed() + } + _ => { + info!("Not using TLS for API"); + TcpListener::bind(address).boxed() + } + }; + + let acceptor = runtime + .block_on(async move { listener.into_acceptor().await }) + .context("Failed to bind Poem to address with OS assigned port")?; + + let actual_address = &acceptor.local_addr()[0]; + let actual_address = *actual_address + .as_socket_addr() + .context("Failed to get socket addr from local addr for Poem webserver")?; + + runtime.spawn(async move { + let cors = Cors::new() + .allow_methods(vec![Method::GET, Method::POST]) + .allow_headers(vec![header::CONTENT_TYPE, header::ACCEPT]); + let route = Route::new() + .nest("/", api_service) + .at("/spec.json", spec_json) + .at("/spec.yaml", spec_yaml) + .with(cors) + .around(middleware_log); + Server::new_with_acceptor(acceptor) + .run(route) + .await + .map_err(anyhow::Error::msg) + }); + + info!( + "Poem is running at {}, behind the reverse proxy at the API port", + actual_address + ); + + Ok(actual_address) +} diff --git a/api/src/poem_backend/transactions.rs b/api/src/poem_backend/transactions.rs new file mode 100644 index 0000000000000..303145991c145 --- /dev/null +++ b/api/src/poem_backend/transactions.rs @@ -0,0 +1,116 @@ +// Copyright (c) Aptos +// SPDX-License-Identifier: Apache-2.0 + +// Copyright (c) Aptos +// SPDX-License-Identifier: Apache-2.0 + +use std::convert::TryFrom; +use std::sync::Arc; + +use super::accept_type::AcceptType; +use super::page::Page; +use super::{response::AptosResponseResult, ApiTags, AptosResponse}; +use super::{AptosError, AptosErrorCode, AptosErrorResponse}; +use crate::context::Context; +use crate::failpoint::fail_point_poem; +use anyhow::format_err; +use aptos_api_types::AsConverter; +use aptos_api_types::{LedgerInfo, Transaction, TransactionOnChainData}; +use poem::web::Accept; +use poem_openapi::param::Query; +use poem_openapi::payload::Json; +use poem_openapi::OpenApi; + +// TODO: Make a helper that builds an AptosResponse from just an anyhow error, +// that assumes that it's an internal error. We can use .context() add more info. + +pub struct TransactionsApi { + pub context: Arc, +} + +#[OpenApi] +impl TransactionsApi { + /// Get transactions + /// + /// todo + #[oai( + path = "/transactions", + method = "get", + operation_id = "get_transactions", + tag = "ApiTags::General" + )] + async fn get_transactions( + &self, + accept: Accept, + start: Query>, + limit: Query>, + ) -> AptosResponseResult> { + fail_point_poem("endpoint_get_transactions")?; + let accept_type = AcceptType::try_from(&accept)?; + let page = Page::new(start.0, limit.0); + self.list(&accept_type, page) + } +} + +impl TransactionsApi { + fn list(&self, accept_type: &AcceptType, page: Page) -> AptosResponseResult> { + let latest_ledger_info = self.context.get_latest_ledger_info_poem()?; + let ledger_version = latest_ledger_info.version(); + let limit = page.limit()?; + let last_page_start = if ledger_version > (limit as u64) { + ledger_version - (limit as u64) + } else { + 0 + }; + let start_version = page.start(last_page_start, ledger_version)?; + + let data = self + .context + .get_transactions(start_version, limit, ledger_version) + .map_err(|e| { + AptosErrorResponse::InternalServerError(Json( + AptosError::new( + format_err!("Failed to read raw transactions from storage: {}", e) + .to_string(), + ) + .error_code(AptosErrorCode::InvalidBcsInStorageError), + )) + })?; + + self.render_transactions(data, accept_type, &latest_ledger_info) + } + + fn render_transactions( + &self, + data: Vec, + accept_type: &AcceptType, + latest_ledger_info: &LedgerInfo, + ) -> AptosResponseResult> { + if data.is_empty() { + return AptosResponse::try_from_rust_value(vec![], latest_ledger_info, accept_type); + } + + let resolver = self.context.move_resolver_poem()?; + let converter = resolver.as_converter(); + let txns: Vec = data + .into_iter() + .map(|t| { + let version = t.version; + let timestamp = self.context.get_block_timestamp(version)?; + let txn = converter.try_into_onchain_transaction(timestamp, t)?; + Ok(txn) + }) + .collect::>() + .map_err(|e| { + AptosErrorResponse::InternalServerError(Json( + AptosError::new( + format_err!("Failed to convert transaction data from storage: {}", e) + .to_string(), + ) + .error_code(AptosErrorCode::InvalidBcsInStorageError), + )) + })?; + + AptosResponse::try_from_rust_value(txns, latest_ledger_info, accept_type) + } +} diff --git a/api/src/runtime.rs b/api/src/runtime.rs index 76e60eb20656c..d4a409abb729c 100644 --- a/api/src/runtime.rs +++ b/api/src/runtime.rs @@ -1,16 +1,16 @@ // Copyright (c) Aptos // SPDX-License-Identifier: Apache-2.0 -use crate::{context::Context, index}; - +use crate::{context::Context, index, poem_backend::attach_poem_to_runtime}; +use anyhow::Context as AnyhowContext; use aptos_config::config::{ApiConfig, NodeConfig}; use aptos_mempool::MempoolClientSender; use aptos_types::chain_id::ChainId; -use storage_interface::DbReader; -use warp::{Filter, Reply}; - use std::{convert::Infallible, net::SocketAddr, sync::Arc}; +use storage_interface::DbReader; use tokio::runtime::{Builder, Runtime}; +use warp::{Filter, Reply}; +use warp_reverse_proxy::reverse_proxy_filter; /// Creates HTTP server (warp-based) serves for both REST and JSON-RPC API. /// When api and json-rpc are configured with same port, both API will be served for the port. @@ -27,16 +27,24 @@ pub fn bootstrap( .thread_name("api") .enable_all() .build() - .expect("[api] failed to create runtime"); + .context("[api] failed to create runtime")?; + let context = Context::new(chain_id, db, mp_sender, config.clone()); - let api = WebServer::from(config.api.clone()); - let node_config = config.clone(); + // Poem will run on a different port. + let poem_address = attach_poem_to_runtime(&runtime, context.clone(), config) + .context("Failed to attach poem to runtime")?; + let api = WebServer::from(config.api.clone()); runtime.spawn(async move { - let context = Context::new(chain_id, db, mp_sender, node_config); - let routes = index::routes(context); + // TODO: This proxy is temporary while we have both APIs running. + let proxy = warp::path!("v1" / ..).and(reverse_proxy_filter( + "v1".to_string(), + format!("http://{}", poem_address), + )); + let routes = proxy.or(index::routes(context)); api.serve(routes).await; }); + Ok(runtime) } diff --git a/api/types/Cargo.toml b/api/types/Cargo.toml index 6fe8a4715246c..0014b67ed6b3b 100644 --- a/api/types/Cargo.toml +++ b/api/types/Cargo.toml @@ -11,14 +11,19 @@ edition = "2018" [dependencies] anyhow = "1.0.57" +async-trait = "0.1.53" bcs = "0.1.3" hex = "0.4.3" +mime = "0.3.16" +poem = { version = "1.3.35", features = ["anyhow", "rustls"] } +poem-openapi = { version = "2.0.5", features = ["swagger-ui", "url"] } serde = { version = "1.0.137", default-features = false } serde_json = "1.0.81" warp = { version = "0.3.2", features = ["default"] } aptos-config = { path = "../../config" } aptos-crypto = { path = "../../crates/aptos-crypto" } +aptos-openapi = { path = "../../crates/aptos-openapi" } aptos-state-view = { path = "../../storage/state-view" } aptos-transaction-builder = { path = "../../sdk/transaction-builder" } aptos-types = { path = "../../types" } diff --git a/api/types/src/account.rs b/api/types/src/account.rs index 244ab13248aa5..3bab703d20ab8 100644 --- a/api/types/src/account.rs +++ b/api/types/src/account.rs @@ -4,9 +4,10 @@ use crate::{HexEncodedBytes, U64}; use aptos_types::account_config::AccountResource; +use poem_openapi::Object; use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Object)] pub struct AccountData { pub sequence_number: U64, pub authentication_key: HexEncodedBytes, @@ -14,9 +15,10 @@ pub struct AccountData { impl From for AccountData { fn from(ar: AccountResource) -> Self { + let authentication_key: HexEncodedBytes = ar.authentication_key().to_vec().into(); Self { sequence_number: ar.sequence_number().into(), - authentication_key: ar.authentication_key().to_vec().into(), + authentication_key, } } } diff --git a/api/types/src/address.rs b/api/types/src/address.rs index 1e90fff7b1ea1..7f717e6c36986 100644 --- a/api/types/src/address.rs +++ b/api/types/src/address.rs @@ -1,6 +1,7 @@ // Copyright (c) Aptos // SPDX-License-Identifier: Apache-2.0 +use aptos_openapi::{impl_poem_parameter, impl_poem_type}; use aptos_types::account_address::AccountAddress; use move_deps::move_core_types; use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer}; @@ -75,6 +76,9 @@ impl<'de> Deserialize<'de> for Address { } } +impl_poem_type!(Address); +impl_poem_parameter!(Address); + #[cfg(test)] mod tests { use crate::address::Address; diff --git a/api/types/src/bytecode.rs b/api/types/src/bytecode.rs index 5875f5865543f..596200a84304a 100644 --- a/api/types/src/bytecode.rs +++ b/api/types/src/bytecode.rs @@ -41,7 +41,7 @@ pub trait Bytecode { fn new_move_struct_field(&self, def: &FieldDefinition) -> MoveStructField { MoveStructField { - name: self.identifier_at(def.name).to_owned(), + name: self.identifier_at(def.name).to_owned().into(), typ: self.new_move_type(&def.signature.0), } } @@ -55,8 +55,8 @@ pub trait Bytecode { let m_handle = self.module_handle_at(s_handle.module); MoveStructTag { address: (*self.address_identifier_at(m_handle.address)).into(), - module: self.identifier_at(m_handle.name).to_owned(), - name: self.identifier_at(s_handle.name).to_owned(), + module: self.identifier_at(m_handle.name).to_owned().into(), + name: self.identifier_at(s_handle.name).to_owned().into(), generic_type_params: type_params.iter().map(|t| self.new_move_type(t)).collect(), } } @@ -111,7 +111,7 @@ pub trait Bytecode { .map(MoveStructGenericTypeParam::from) .collect(); MoveStruct { - name, + name: name.into(), is_native, abilities, generic_type_params, @@ -123,7 +123,7 @@ pub trait Bytecode { let fhandle = self.function_handle_at(def.function); let name = self.identifier_at(fhandle.name).to_owned(); MoveFunction { - name, + name: name.into(), visibility: def.visibility.into(), is_entry: def.is_entry, generic_type_params: fhandle diff --git a/api/types/src/convert.rs b/api/types/src/convert.rs index 9b6ab7ac07311..e01aefd500c19 100644 --- a/api/types/src/convert.rs +++ b/api/types/src/convert.rs @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{ - transaction::{ModuleBundlePayload, StateCheckpointTransaction}, + transaction::{ + DeleteModule, DeleteResource, DeleteTableItem, ModuleBundlePayload, + StateCheckpointTransaction, WriteModule, WriteResource, WriteTableItem, + }, Bytecode, DirectWriteSet, Event, HexEncodedBytes, MoveFunction, MoveModuleBytecode, MoveResource, MoveScriptBytecode, MoveValue, ScriptFunctionId, ScriptFunctionPayload, ScriptPayload, ScriptWriteSet, Transaction, TransactionInfo, TransactionOnChainData, @@ -167,7 +170,7 @@ impl<'a, R: MoveResolverExt + ?Sized> MoveConverter<'a, R> { arguments: json_args, function: ScriptFunctionId { module: module.into(), - name: function, + name: function.into(), }, type_arguments: ty_args.into_iter().map(|arg| arg.into()).collect(), }) @@ -234,28 +237,28 @@ impl<'a, R: MoveResolverExt + ?Sized> MoveConverter<'a, R> { ) -> Result { let ret = match op { WriteOp::Deletion => match access_path.get_path() { - Path::Code(module_id) => WriteSetChange::DeleteModule { + Path::Code(module_id) => WriteSetChange::DeleteModule(DeleteModule { address: access_path.address.into(), state_key_hash, module: module_id.into(), - }, - Path::Resource(typ) => WriteSetChange::DeleteResource { + }), + Path::Resource(typ) => WriteSetChange::DeleteResource(DeleteResource { address: access_path.address.into(), state_key_hash, resource: typ.into(), - }, + }), }, WriteOp::Value(val) => match access_path.get_path() { - Path::Code(_) => WriteSetChange::WriteModule { + Path::Code(_) => WriteSetChange::WriteModule(WriteModule { address: access_path.address.into(), state_key_hash, data: MoveModuleBytecode::new(val).try_parse_abi()?, - }, - Path::Resource(typ) => WriteSetChange::WriteResource { + }), + Path::Resource(typ) => WriteSetChange::WriteResource(WriteResource { address: access_path.address.into(), state_key_hash, data: self.try_into_resource(&typ, &val)?, - }, + }), }, // Deltas never use access paths. WriteOp::Delta(..) => unreachable!("unexpected conversion"), @@ -273,17 +276,17 @@ impl<'a, R: MoveResolverExt + ?Sized> MoveConverter<'a, R> { let handle = handle.0.to_be_bytes().to_vec().into(); let key = key.into(); let ret = match op { - WriteOp::Deletion => WriteSetChange::DeleteTableItem { + WriteOp::Deletion => WriteSetChange::DeleteTableItem(DeleteTableItem { state_key_hash, handle, key, - }, - WriteOp::Value(value) => WriteSetChange::WriteTableItem { + }), + WriteOp::Value(value) => WriteSetChange::WriteTableItem(WriteTableItem { state_key_hash, handle, key, value: value.into(), - }, + }), // Deltas are materialized into WriteOP::Value(..) in executor. WriteOp::Delta(..) => unreachable!("unexpected conversion"), }; @@ -358,7 +361,7 @@ impl<'a, R: MoveResolverExt + ?Sized> MoveConverter<'a, R> { let module = function.module.clone(); let code = self.inner.get_module(&module.clone().into())? as Rc; let func = code - .find_script_function(function.name.as_ident_str()) + .find_script_function(function.name.0.as_ident_str()) .ok_or_else(|| format_err!("could not find script function by {}", function))?; ensure!( func.generic_type_params.len() == type_arguments.len(), @@ -375,7 +378,7 @@ impl<'a, R: MoveResolverExt + ?Sized> MoveConverter<'a, R> { Target::ScriptFunction(ScriptFunction::new( module.into(), - function.name, + function.name.into(), type_arguments .into_iter() .map(|v| v.try_into()) diff --git a/api/types/src/event_key.rs b/api/types/src/event_key.rs index 31e4f3f8cc6b1..f63d410848b1c 100644 --- a/api/types/src/event_key.rs +++ b/api/types/src/event_key.rs @@ -1,6 +1,7 @@ // Copyright (c) Aptos // SPDX-License-Identifier: Apache-2.0 +use aptos_openapi::{impl_poem_parameter, impl_poem_type}; use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer}; use std::{fmt, str::FromStr}; @@ -51,6 +52,9 @@ impl fmt::Display for EventKey { } } +impl_poem_type!(EventKey); +impl_poem_parameter!(EventKey); + #[cfg(test)] mod tests { use crate::event_key::EventKey; diff --git a/api/types/src/hash.rs b/api/types/src/hash.rs index e51cce90d7061..f955a74722cb4 100644 --- a/api/types/src/hash.rs +++ b/api/types/src/hash.rs @@ -1,6 +1,7 @@ // Copyright (c) Aptos // SPDX-License-Identifier: Apache-2.0 +use aptos_openapi::impl_poem_type; use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer}; use std::{ fmt, @@ -63,6 +64,8 @@ impl LowerHex for HashValue { } } +impl_poem_type!(HashValue); + #[cfg(test)] mod tests { use crate::hash::HashValue; diff --git a/api/types/src/index.rs b/api/types/src/index.rs index bc33f3bd41a6b..52eb732b3b1aa 100644 --- a/api/types/src/index.rs +++ b/api/types/src/index.rs @@ -3,13 +3,17 @@ use crate::LedgerInfo; use aptos_config::config::RoleType; +use poem_openapi::Object as PoemObject; use serde::{Deserialize, Serialize}; +// The data in IndexResponse is flattened into a single JSON map to offer +// easier parsing for clients. + /// The struct holding all data returned to the client by the -/// index endpoint (i.e., GET "/"). The data is flattened into -/// a single JSON map to offer easier parsing for clients. -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +/// index endpoint (i.e., GET "/"). +#[derive(Clone, Debug, Deserialize, PartialEq, PoemObject, Serialize)] pub struct IndexResponse { + #[oai(flatten)] #[serde(flatten)] pub ledger_info: LedgerInfo, pub node_role: RoleType, diff --git a/api/types/src/ledger_info.rs b/api/types/src/ledger_info.rs index 5e092c26e952b..7a2e43dcc1daa 100644 --- a/api/types/src/ledger_info.rs +++ b/api/types/src/ledger_info.rs @@ -4,10 +4,11 @@ use crate::U64; use aptos_types::{chain_id::ChainId, ledger_info::LedgerInfoWithSignatures}; +use poem_openapi::Object as PoemObject; use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, PoemObject)] pub struct LedgerInfo { pub chain_id: u8, pub epoch: u64, diff --git a/api/types/src/lib.rs b/api/types/src/lib.rs index 0974603a2fa35..6b8360da0c471 100644 --- a/api/types/src/lib.rs +++ b/api/types/src/lib.rs @@ -16,6 +16,7 @@ mod move_types; mod response; mod table; mod transaction; +mod wrappers; pub use account::AccountData; pub use address::Address; @@ -37,9 +38,11 @@ pub use response::{ }; pub use table::TableItemRequest; pub use transaction::{ - BlockMetadataTransaction, DirectWriteSet, Event, GenesisTransaction, PendingTransaction, - ScriptFunctionPayload, ScriptPayload, ScriptWriteSet, Transaction, TransactionData, - TransactionId, TransactionInfo, TransactionOnChainData, TransactionPayload, - TransactionSigningMessage, UserCreateSigningMessageRequest, UserTransaction, - UserTransactionRequest, WriteSet, WriteSetChange, WriteSetPayload, + BlockMetadataTransaction, DeleteModule, DeleteResource, DeleteTableItem, DirectWriteSet, Event, + GenesisTransaction, PendingTransaction, ScriptFunctionPayload, ScriptPayload, ScriptWriteSet, + Transaction, TransactionData, TransactionId, TransactionInfo, TransactionOnChainData, + TransactionPayload, TransactionSigningMessage, UserCreateSigningMessageRequest, + UserTransaction, UserTransactionRequest, WriteModule, WriteResource, WriteSet, WriteSetChange, + WriteSetPayload, WriteTableItem, }; +pub use wrappers::{IdentifierWrapper, MoveStructTagWrapper}; diff --git a/api/types/src/move_types.rs b/api/types/src/move_types.rs index 97e3f421a9db7..bf6eb732c64e7 100644 --- a/api/types/src/move_types.rs +++ b/api/types/src/move_types.rs @@ -1,9 +1,9 @@ // Copyright (c) Aptos // SPDX-License-Identifier: Apache-2.0 -use crate::{Address, Bytecode}; - +use crate::{Address, Bytecode, IdentifierWrapper}; use anyhow::{bail, format_err}; +use aptos_openapi::{impl_poem_parameter, impl_poem_type}; use aptos_types::{account_config::CORE_CODE_ADDRESS, event::EventKey, transaction::Module}; use move_deps::{ move_binary_format::{ @@ -23,6 +23,7 @@ use move_deps::{ move_resource_viewer::{AnnotatedMoveStruct, AnnotatedMoveValue}, }; +use poem_openapi::{Enum, NewType, Object}; use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer}; use std::{ collections::BTreeMap, @@ -32,9 +33,10 @@ use std::{ str::FromStr, }; -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Object)] pub struct MoveResource { #[serde(rename = "type")] + #[oai(rename = "type")] pub typ: MoveStructTag, pub data: MoveStructValue, } @@ -50,7 +52,7 @@ impl TryFrom for MoveResource { } } -#[derive(Clone, Debug, Eq, PartialEq, Copy)] +#[derive(Clone, Debug, Default, Eq, PartialEq, Copy, NewType)] pub struct U64(pub u64); impl U64 { @@ -117,7 +119,7 @@ impl<'de> Deserialize<'de> for U64 { } } -#[derive(Clone, Debug, PartialEq, Copy)] +#[derive(Clone, Debug, Default, PartialEq, Copy)] pub struct U128(u128); impl U128 { @@ -249,14 +251,14 @@ impl HexEncodedBytes { } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct MoveStructValue(pub BTreeMap); +pub struct MoveStructValue(pub BTreeMap); impl TryFrom for MoveStructValue { type Error = anyhow::Error; fn try_from(s: AnnotatedMoveStruct) -> anyhow::Result { let mut map = BTreeMap::new(); for (id, val) in s.value { - map.insert(id, MoveValue::try_from(val)?.json()?); + map.insert(id.into(), MoveValue::try_from(val)?.json()?); } Ok(Self(map)) } @@ -351,19 +353,19 @@ impl Serialize for MoveValue { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Object)] pub struct MoveStructTag { pub address: Address, - pub module: Identifier, - pub name: Identifier, + pub module: IdentifierWrapper, + pub name: IdentifierWrapper, pub generic_type_params: Vec, } impl MoveStructTag { pub fn new( address: Address, - module: Identifier, - name: Identifier, + module: IdentifierWrapper, + name: IdentifierWrapper, generic_type_params: Vec, ) -> Self { Self { @@ -387,8 +389,8 @@ impl From for MoveStructTag { fn from(tag: StructTag) -> Self { Self { address: tag.address.into(), - module: tag.module, - name: tag.name, + module: tag.module.into(), + name: tag.name.into(), generic_type_params: tag.type_params.into_iter().map(MoveType::from).collect(), } } @@ -430,8 +432,8 @@ impl TryFrom for StructTag { fn try_from(tag: MoveStructTag) -> anyhow::Result { Ok(Self { address: tag.address.into(), - module: tag.module, - name: tag.name, + module: tag.module.into(), + name: tag.name.into(), type_params: tag .generic_type_params .into_iter() @@ -583,10 +585,10 @@ impl TryFrom for TypeTag { } } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Object)] pub struct MoveModule { pub address: Address, - pub name: Identifier, + pub name: IdentifierWrapper, pub friends: Vec, pub exposed_functions: Vec, pub structs: Vec, @@ -597,7 +599,7 @@ impl From for MoveModule { let (address, name) = <(AccountAddress, Identifier)>::from(m.self_id()); Self { address: address.into(), - name, + name: name.into(), friends: m .immediate_friends() .into_iter() @@ -621,10 +623,10 @@ impl From for MoveModule { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Object)] pub struct MoveModuleId { pub address: Address, - pub name: Identifier, + pub name: IdentifierWrapper, } impl From for MoveModuleId { @@ -632,14 +634,14 @@ impl From for MoveModuleId { let (address, name) = <(AccountAddress, Identifier)>::from(id); Self { address: address.into(), - name, + name: name.into(), } } } impl From for ModuleId { fn from(id: MoveModuleId) -> Self { - ModuleId::new(id.address.into(), id.name) + ModuleId::new(id.address.into(), id.name.into()) } } @@ -684,15 +686,18 @@ impl<'de> Deserialize<'de> for MoveModuleId { } } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Object)] pub struct MoveStruct { - pub name: Identifier, + pub name: IdentifierWrapper, pub is_native: bool, pub abilities: Vec, pub generic_type_params: Vec, pub fields: Vec, } +// TODO: Consider finding a way to derive NewType here instead of using the +// custom macro, since some of the enum type information (such as the +// variants) is currently being lost. #[derive(Clone, Debug, PartialEq)] pub struct MoveAbility(Ability); @@ -750,7 +755,7 @@ impl<'de> Deserialize<'de> for MoveAbility { } } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Object)] pub struct MoveStructGenericTypeParam { pub constraints: Vec, pub is_phantom: bool, @@ -769,28 +774,30 @@ impl From<&StructTypeParameter> for MoveStructGenericTypeParam { } } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Object)] pub struct MoveStructField { - pub name: Identifier, + pub name: IdentifierWrapper, #[serde(rename = "type")] + #[oai(rename = "type")] pub typ: MoveType, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Object)] pub struct MoveFunction { - pub name: Identifier, + pub name: IdentifierWrapper, pub visibility: MoveFunctionVisibility, pub is_entry: bool, pub generic_type_params: Vec, pub params: Vec, #[serde(rename = "return")] + #[oai(rename = "return")] pub return_: Vec, } impl From<&CompiledScript> for MoveFunction { fn from(script: &CompiledScript) -> Self { Self { - name: Identifier::new("main").unwrap(), + name: Identifier::new("main").unwrap().into(), visibility: MoveFunctionVisibility::Public, is_entry: true, generic_type_params: script @@ -809,7 +816,7 @@ impl From<&CompiledScript> for MoveFunction { } } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Enum)] #[serde(rename_all = "snake_case")] pub enum MoveFunctionVisibility { Private, @@ -837,7 +844,7 @@ impl From for Visibility { } } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Object)] pub struct MoveFunctionGenericTypeParam { pub constraints: Vec, } @@ -850,7 +857,7 @@ impl From<&AbilitySet> for MoveFunctionGenericTypeParam { } } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Object)] pub struct MoveModuleBytecode { pub bytecode: HexEncodedBytes, // We don't need deserialize MoveModule as it should be serialized @@ -886,7 +893,7 @@ impl From for MoveModuleBytecode { } } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Object)] pub struct MoveScriptBytecode { pub bytecode: HexEncodedBytes, // We don't need deserialize MoveModule as it should be serialized @@ -916,10 +923,10 @@ impl MoveScriptBytecode { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Object)] pub struct ScriptFunctionId { pub module: MoveModuleId, - pub name: Identifier, + pub name: IdentifierWrapper, } impl FromStr for ScriptFunctionId { @@ -1302,3 +1309,12 @@ mod tests { serde_json::to_string_pretty(val).unwrap() } } + +// This macro derives all the necessary traits to make it possible to use the +// given types in a Poem API. This macro in many ways is a short cut compared +// to deriving the traits properly (e.g. #[derive(Type)] on a struct), use it +// with great caution, since it essentially rewrites the type to be a string +// from the perspective of the OpenAPI spec, potentially losing some useful +// type information that the client could use. +impl_poem_type!(MoveAbility, MoveStructValue, MoveType, HexEncodedBytes); +impl_poem_parameter!(HexEncodedBytes); diff --git a/api/types/src/transaction.rs b/api/types/src/transaction.rs index 7fd6b4a3c822e..73ad885928aff 100755 --- a/api/types/src/transaction.rs +++ b/api/types/src/transaction.rs @@ -21,6 +21,7 @@ use aptos_types::{ }, }; +use poem_openapi::{Object, Union}; use serde::{Deserialize, Serialize}; use std::{ boxed::Box, @@ -29,6 +30,8 @@ use std::{ str::FromStr, }; +// TODO: Add read_only / write_only (and their all variants) where appropriate. + #[derive(Clone, Debug, Deserialize, Serialize)] pub enum TransactionData { OnChain(TransactionOnChainData), @@ -126,7 +129,8 @@ impl } } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Union)] +#[oai(discriminator_name = "type")] #[serde(tag = "type", rename_all = "snake_case")] pub enum Transaction { PendingTransaction(PendingTransaction), @@ -280,7 +284,7 @@ impl From<(&SignedTransaction, TransactionPayload)> for UserTransactionRequest { } } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Object)] pub struct TransactionInfo { pub version: U64, pub hash: HashValue, @@ -293,31 +297,35 @@ pub struct TransactionInfo { pub changes: Vec, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Object)] pub struct PendingTransaction { pub hash: HashValue, #[serde(flatten)] + #[oai(flatten)] pub request: UserTransactionRequest, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Object)] pub struct UserTransaction { #[serde(flatten)] + #[oai(flatten)] pub info: TransactionInfo, #[serde(flatten)] + #[oai(flatten)] pub request: UserTransactionRequest, pub events: Vec, pub timestamp: U64, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Object)] pub struct StateCheckpointTransaction { #[serde(flatten)] + #[oai(flatten)] pub info: TransactionInfo, pub timestamp: U64, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Object)] pub struct UserTransactionRequest { pub sender: Address, pub sequence_number: U64, @@ -329,25 +337,28 @@ pub struct UserTransactionRequest { pub signature: Option, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Object)] pub struct UserCreateSigningMessageRequest { #[serde(flatten)] + #[oai(flatten)] pub transaction: UserTransactionRequest, #[serde(skip_serializing_if = "Option::is_none")] pub secondary_signers: Option>, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Object)] pub struct GenesisTransaction { #[serde(flatten)] + #[oai(flatten)] pub info: TransactionInfo, pub payload: GenesisPayload, pub events: Vec, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Object)] pub struct BlockMetadataTransaction { #[serde(flatten)] + #[oai(flatten)] pub info: TransactionInfo, pub id: HashValue, pub epoch: U64, @@ -359,12 +370,14 @@ pub struct BlockMetadataTransaction { pub timestamp: U64, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Object)] pub struct Event { pub key: EventKey, pub sequence_number: U64, #[serde(rename = "type")] + #[oai(rename = "type")] pub typ: MoveType, + // TODO: Use the real data here, not a JSON representation. pub data: serde_json::Value, } @@ -381,13 +394,15 @@ impl From<(&ContractEvent, serde_json::Value)> for Event { } } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Union)] +#[oai(discriminator_name = "type")] #[serde(tag = "type", rename_all = "snake_case")] pub enum GenesisPayload { WriteSetPayload(WriteSetPayload), } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Union)] +#[oai(discriminator_name = "type")] #[serde(tag = "type", rename_all = "snake_case")] pub enum TransactionPayload { ScriptFunctionPayload(ScriptFunctionPayload), @@ -396,22 +411,24 @@ pub enum TransactionPayload { WriteSetPayload(WriteSetPayload), } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Object)] pub struct ModuleBundlePayload { pub modules: Vec, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Object)] pub struct ScriptFunctionPayload { pub function: ScriptFunctionId, pub type_arguments: Vec, + // TODO: Use the real data here, not a JSON representation. pub arguments: Vec, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Object)] pub struct ScriptPayload { pub code: MoveScriptBytecode, pub type_arguments: Vec, + // TODO: Use the real data here, not a JSON representation. pub arguments: Vec, } @@ -431,64 +448,84 @@ impl TryFrom