From c1e6646f578c304f6aa29b47ff6cc27f1a1de6dc Mon Sep 17 00:00:00 2001 From: 0xterminator Date: Tue, 11 Mar 2025 02:06:44 +0000 Subject: [PATCH] feat(repo): Added open-api documentation (#428) * feat(repo): Added rest endpoints Postman documentation * feat(repo): Added open-api documentation --- Cargo.lock | 155 +++++++++++++++ crates/core/Cargo.toml | 1 + crates/core/src/server/responses.rs | 30 ++- crates/domains/Cargo.toml | 1 + crates/domains/src/blocks/queryable.rs | 22 ++- crates/domains/src/blocks/types.rs | 21 ++- crates/domains/src/inputs/queryable.rs | 4 +- crates/domains/src/inputs/types.rs | 16 +- crates/domains/src/outputs/queryable.rs | 4 +- crates/domains/src/outputs/types.rs | 24 ++- crates/domains/src/queryable.rs | 16 +- crates/domains/src/receipts/queryable.rs | 4 +- crates/domains/src/receipts/types.rs | 56 ++++-- crates/domains/src/transactions/queryable.rs | 4 +- crates/domains/src/transactions/types.rs | 122 +++++++++++- crates/domains/src/utxos/queryable.rs | 4 +- crates/domains/src/utxos/types.rs | 11 +- crates/store/Cargo.toml | 1 + crates/store/src/record/record_packet.rs | 2 +- crates/types/Cargo.toml | 1 + crates/types/src/primitives/amount.rs | 9 +- crates/types/src/primitives/block_header.rs | 33 +++- crates/types/src/primitives/block_height.rs | 13 +- .../types/src/primitives/block_timestamp.rs | 20 ++ crates/types/src/primitives/bytes.rs | 59 ++++++ crates/types/src/primitives/bytes_long.rs | 1 + .../types/src/primitives/da_block_height.rs | 9 +- crates/types/src/primitives/gas_amount.rs | 9 +- crates/types/src/primitives/mod.rs | 1 + crates/types/src/primitives/open_api.rs | 177 ++++++++++++++++++ .../types/src/primitives/script_execution.rs | 104 ++++++++++ crates/types/src/primitives/tx_pointer.rs | 1 + crates/types/src/primitives/tx_status.rs | 4 +- crates/types/src/primitives/tx_type.rs | 4 +- crates/types/src/primitives/utxo_id.rs | 1 + crates/types/src/primitives/word.rs | 9 +- crates/types/src/primitives/wrapped_int.rs | 16 ++ scripts/run_api.sh | 2 +- services/api/Cargo.toml | 2 + services/api/src/main.rs | 1 + services/api/src/server/handlers/accounts.rs | 138 ++++++++++++++ services/api/src/server/handlers/blocks.rs | 170 ++++++++++++++++- services/api/src/server/handlers/contracts.rs | 134 +++++++++++++ services/api/src/server/handlers/inputs.rs | 39 ++++ services/api/src/server/handlers/mod.rs | 13 +- services/api/src/server/handlers/open_api.rs | 148 +++++++++++++++ services/api/src/server/handlers/outputs.rs | 37 ++++ services/api/src/server/handlers/receipts.rs | 42 +++++ .../api/src/server/handlers/transactions.rs | 143 ++++++++++++++ services/api/src/server/handlers/utxos.rs | 37 ++++ 50 files changed, 1808 insertions(+), 67 deletions(-) create mode 100644 crates/types/src/primitives/open_api.rs create mode 100644 services/api/src/server/handlers/open_api.rs diff --git a/Cargo.lock b/Cargo.lock index 7f044d56..89535fd2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -431,6 +431,9 @@ name = "arbitrary" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] [[package]] name = "archery" @@ -2209,6 +2212,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "derive_more" version = "0.99.18" @@ -3816,6 +3830,7 @@ dependencies = [ "thiserror 2.0.11", "tokio", "tracing", + "utoipa", ] [[package]] @@ -3844,6 +3859,7 @@ dependencies = [ "test-case", "thiserror 2.0.11", "tokio", + "utoipa", ] [[package]] @@ -3871,6 +3887,7 @@ dependencies = [ "thiserror 2.0.11", "tokio", "tracing", + "utoipa", ] [[package]] @@ -3937,6 +3954,7 @@ dependencies = [ "thiserror 2.0.11", "tokio", "tracing", + "utoipa", ] [[package]] @@ -6187,6 +6205,12 @@ dependencies = [ "serde", ] +[[package]] +name = "lockfree-object-pool" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" + [[package]] name = "log" version = "0.4.26" @@ -6351,6 +6375,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -8388,6 +8422,40 @@ dependencies = [ "tokio", ] +[[package]] +name = "rust-embed" +version = "8.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b3aba5104622db5c9fc61098de54708feb732e7763d7faa2fa625899f00bf6f" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f198c73be048d2c5aa8e12f7960ad08443e56fd39cc26336719fdb4ea0ebaae" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.98", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a2fcdc9f40c8dc2922842ca9add611ad19f332227fc651d015881ad1552bd9a" +dependencies = [ + "sha2 0.10.8", + "walkdir", +] + [[package]] name = "rust_decimal" version = "1.36.0" @@ -9031,6 +9099,12 @@ dependencies = [ "wide", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "simdutf8" version = "0.1.5" @@ -9561,6 +9635,8 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "utoipa", + "utoipa-swagger-ui", "validator", ] @@ -10687,6 +10763,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-bidi" version = "0.3.18" @@ -10811,6 +10893,48 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "utoipa" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435c6f69ef38c9017b4b4eea965dfb91e71e53d869e896db40d1cf2441dd75c0" +dependencies = [ + "indexmap 2.7.1", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77d306bc75294fd52f3e99b13ece67c02c1a2789190a6f31d32f736624326f7" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.98", +] + +[[package]] +name = "utoipa-swagger-ui" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "161166ec520c50144922a625d8bc4925cc801b2dda958ab69878527c0e5c5d61" +dependencies = [ + "actix-web", + "base64 0.22.1", + "mime_guess", + "regex", + "rust-embed", + "serde", + "serde_json", + "url", + "utoipa", + "zip", +] + [[package]] name = "uuid" version = "1.15.1" @@ -11840,6 +11964,37 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "zip" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b280484c454e74e5fff658bbf7df8fdbe7a07c6b2de4a53def232c15ef138f3a" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "flate2", + "indexmap 2.7.1", + "memchr", + "thiserror 2.0.11", + "zopfli", +] + +[[package]] +name = "zopfli" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +dependencies = [ + "bumpalo", + "crc32fast", + "lockfree-object-pool", + "log", + "once_cell", + "simd-adler32", +] + [[package]] name = "zstd" version = "0.13.3" diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index d30441cc..cd076259 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -30,6 +30,7 @@ serde_json.workspace = true thiserror.workspace = true tokio.workspace = true tracing.workspace = true +utoipa = { version = "5.3.1", features = ["actix_extras"] } [dev-dependencies] pretty_assertions.workspace = true diff --git a/crates/core/src/server/responses.rs b/crates/core/src/server/responses.rs index c71e2213..066e3d41 100644 --- a/crates/core/src/server/responses.rs +++ b/crates/core/src/server/responses.rs @@ -46,6 +46,34 @@ pub enum MessagePayload { Utxo(Arc), } +impl utoipa::ToSchema for MessagePayload { + fn name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("MessagePayload") + } +} + +impl utoipa::PartialSchema for MessagePayload { + fn schema() -> utoipa::openapi::RefOr { + // Create a oneOf schema + let mut one_of = utoipa::openapi::schema::OneOf::new(); + + // Add references to all possible payload types + // We can use the schema() method directly from each type + one_of.items.push(Block::schema()); + one_of.items.push(Input::schema()); + one_of.items.push(Output::schema()); + one_of.items.push(Transaction::schema()); + one_of.items.push(Receipt::schema()); + one_of.items.push(Utxo::schema()); + + // Build the oneOf schema with a description + let schema = utoipa::openapi::schema::Schema::OneOf(one_of); + + // Return the schema wrapped in RefOr::T + utoipa::openapi::RefOr::T(schema) + } +} + impl MessagePayload { pub fn new( subject_id: &str, @@ -133,7 +161,7 @@ pub enum StreamResponseError { RecordPacket(#[from] RecordPacketError), } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct StreamResponse { pub version: String, #[serde(rename = "type")] diff --git a/crates/domains/Cargo.toml b/crates/domains/Cargo.toml index b0e63acf..76adfe00 100644 --- a/crates/domains/Cargo.toml +++ b/crates/domains/Cargo.toml @@ -30,6 +30,7 @@ serde_with = "3.12.0" sqlx.workspace = true thiserror.workspace = true tokio.workspace = true +utoipa = { version = "5.3.1", features = ["actix_extras"] } [dev-dependencies] pretty_assertions.workspace = true diff --git a/crates/domains/src/blocks/queryable.rs b/crates/domains/src/blocks/queryable.rs index 52241ff9..5027496c 100644 --- a/crates/domains/src/blocks/queryable.rs +++ b/crates/domains/src/blocks/queryable.rs @@ -12,7 +12,16 @@ use serde::{Deserialize, Serialize}; use super::{types::*, BlockDbItem}; use crate::queryable::{HasPagination, QueryPagination, Queryable}; -#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[derive( + Debug, + Default, + Clone, + Serialize, + Deserialize, + PartialEq, + Eq, + utoipa::ToSchema, +)] pub enum TimeRange { #[serde(rename = "1h")] OneHour, @@ -121,7 +130,16 @@ pub enum Blocks { BlockPropagationMs, } -#[derive(Debug, Clone, Default, Serialize, Deserialize, Eq, PartialEq)] +#[derive( + Debug, + Clone, + Default, + Serialize, + Deserialize, + Eq, + PartialEq, + utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct BlocksQuery { pub producer: Option
, diff --git a/crates/domains/src/blocks/types.rs b/crates/domains/src/blocks/types.rs index 2a91ae8e..25861f8e 100644 --- a/crates/domains/src/blocks/types.rs +++ b/crates/domains/src/blocks/types.rs @@ -3,7 +3,7 @@ use fuel_streams_types::{fuel_core::*, primitives::*}; use serde::{Deserialize, Serialize}; // Block type -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct Block { pub consensus: Consensus, @@ -43,14 +43,23 @@ impl Block { } // Consensus enum -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, utoipa::ToSchema)] #[serde(tag = "type")] pub enum Consensus { Genesis(Genesis), PoAConsensus(PoAConsensus), } -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[derive( + Clone, + Debug, + Default, + PartialEq, + Eq, + Serialize, + Deserialize, + utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct Genesis { pub chain_config_hash: Bytes32, @@ -72,7 +81,9 @@ impl From for Genesis { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive( + Debug, Clone, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema, +)] pub struct PoAConsensus { pub signature: Signature, } @@ -110,7 +121,7 @@ impl From for Consensus { } // BlockVersion enum -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, utoipa::ToSchema)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum BlockVersion { V1, diff --git a/crates/domains/src/inputs/queryable.rs b/crates/domains/src/inputs/queryable.rs index 4a5e0c9a..30b3e806 100644 --- a/crates/domains/src/inputs/queryable.rs +++ b/crates/domains/src/inputs/queryable.rs @@ -41,7 +41,9 @@ pub enum Inputs { PublishedAt, } -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +#[derive( + Debug, Clone, Default, Serialize, Deserialize, PartialEq, utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct InputsQuery { pub tx_id: Option, diff --git a/crates/domains/src/inputs/types.rs b/crates/domains/src/inputs/types.rs index 5f667b58..89177923 100644 --- a/crates/domains/src/inputs/types.rs +++ b/crates/domains/src/inputs/types.rs @@ -1,7 +1,7 @@ use fuel_streams_types::{fuel_core::*, primitives::*}; use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, utoipa::ToSchema)] #[serde(tag = "type")] pub enum Input { Contract(InputContract), @@ -122,7 +122,9 @@ impl Default for Input { } // InputCoin type -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[derive( + Debug, Clone, Default, PartialEq, Serialize, Deserialize, utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct InputCoin { pub amount: Amount, @@ -137,7 +139,9 @@ pub struct InputCoin { } // InputContract type -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[derive( + Debug, Clone, Default, PartialEq, Serialize, Deserialize, utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct InputContract { pub balance_root: Bytes32, @@ -160,7 +164,9 @@ impl From<&FuelCoreInputContract> for InputContract { } // InputMessage type -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[derive( + Debug, Clone, Default, PartialEq, Serialize, Deserialize, utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct InputMessage { pub amount: Amount, @@ -197,7 +203,7 @@ impl InputMessage { } } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, utoipa::ToSchema)] pub enum InputType { Contract, Coin, diff --git a/crates/domains/src/outputs/queryable.rs b/crates/domains/src/outputs/queryable.rs index f4959300..6d2a582f 100644 --- a/crates/domains/src/outputs/queryable.rs +++ b/crates/domains/src/outputs/queryable.rs @@ -37,7 +37,9 @@ pub enum Outputs { PublishedAt, } -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +#[derive( + Debug, Clone, Default, Serialize, Deserialize, PartialEq, utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct OutputsQuery { pub tx_id: Option, diff --git a/crates/domains/src/outputs/types.rs b/crates/domains/src/outputs/types.rs index 798806d4..729fec63 100644 --- a/crates/domains/src/outputs/types.rs +++ b/crates/domains/src/outputs/types.rs @@ -2,7 +2,7 @@ use fuel_streams_types::{fuel_core::*, primitives::*}; use serde::{Deserialize, Serialize}; // Output enum -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, utoipa::ToSchema)] #[serde(tag = "type")] pub enum Output { Coin(OutputCoin), @@ -56,7 +56,9 @@ impl From<&FuelCoreOutput> for Output { } } -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[derive( + Debug, Clone, Default, PartialEq, Serialize, Deserialize, utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct OutputCoin { pub amount: Amount, @@ -64,7 +66,9 @@ pub struct OutputCoin { pub to: Address, } -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[derive( + Debug, Clone, Default, PartialEq, Serialize, Deserialize, utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct OutputChange { pub amount: Amount, @@ -72,7 +76,9 @@ pub struct OutputChange { pub to: Address, } -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[derive( + Debug, Clone, Default, PartialEq, Serialize, Deserialize, utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct OutputVariable { pub amount: Amount, @@ -80,7 +86,9 @@ pub struct OutputVariable { pub to: Address, } -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[derive( + Debug, Clone, Default, PartialEq, Serialize, Deserialize, utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct OutputContract { pub balance_root: Bytes32, @@ -98,14 +106,16 @@ impl From<&FuelCoreOutputContract> for OutputContract { } } -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[derive( + Debug, Clone, Default, PartialEq, Serialize, Deserialize, utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct OutputContractCreated { pub contract_id: ContractId, pub state_root: Bytes32, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, utoipa::ToSchema)] pub enum OutputType { Coin, Contract, diff --git a/crates/domains/src/queryable.rs b/crates/domains/src/queryable.rs index 4d23ed2d..36eb8e07 100644 --- a/crates/domains/src/queryable.rs +++ b/crates/domains/src/queryable.rs @@ -14,13 +14,16 @@ use sea_query::{ }; use serde::{Deserialize, Serialize}; use serde_with::{serde_as, DisplayFromStr}; +use utoipa::ToSchema; pub const MAX_FIRST: i32 = 100; pub const MAX_LAST: i32 = 100; // NOTE: https://docs.rs/serde_qs/0.14.0/serde_qs/index.html#flatten-workaround #[serde_as] -#[derive(Debug, Clone, Default, Serialize, Deserialize, Eq, PartialEq)] +#[derive( + Debug, Clone, Default, Serialize, Deserialize, Eq, PartialEq, ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct QueryPagination { #[serde_as(as = "Option")] @@ -157,7 +160,16 @@ pub trait Queryable: Sized + 'static { } } -#[derive(Debug, Clone, Default, Serialize, Deserialize, Eq, PartialEq)] +#[derive( + Debug, + Clone, + Default, + Serialize, + Deserialize, + Eq, + PartialEq, + utoipa::ToSchema, +)] pub struct ValidatedQuery(pub T); impl ValidatedQuery { diff --git a/crates/domains/src/receipts/queryable.rs b/crates/domains/src/receipts/queryable.rs index 4fa5f7ff..99d5eafa 100644 --- a/crates/domains/src/receipts/queryable.rs +++ b/crates/domains/src/receipts/queryable.rs @@ -48,7 +48,9 @@ pub enum Receipts { PublishedAt, } -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +#[derive( + Debug, Clone, Default, Serialize, Deserialize, PartialEq, utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct ReceiptsQuery { pub tx_id: Option, diff --git a/crates/domains/src/receipts/types.rs b/crates/domains/src/receipts/types.rs index 7962c5df..fac742be 100644 --- a/crates/domains/src/receipts/types.rs +++ b/crates/domains/src/receipts/types.rs @@ -1,7 +1,7 @@ use fuel_streams_types::{fuel_core::*, primitives::*}; use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, utoipa::ToSchema)] #[serde(tag = "type")] pub enum Receipt { Call(CallReceipt), @@ -30,7 +30,9 @@ impl Receipt { } // Individual Receipt Types -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[derive( + Debug, Clone, Default, PartialEq, Serialize, Deserialize, utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct CallReceipt { pub id: ContractId, @@ -44,7 +46,9 @@ pub struct CallReceipt { pub is: Word, } -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[derive( + Debug, Clone, Default, PartialEq, Serialize, Deserialize, utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct ReturnReceipt { pub id: ContractId, @@ -53,7 +57,9 @@ pub struct ReturnReceipt { pub is: Word, } -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[derive( + Debug, Clone, Default, PartialEq, Serialize, Deserialize, utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct ReturnDataReceipt { pub id: ContractId, @@ -65,7 +71,9 @@ pub struct ReturnDataReceipt { pub data: Option, } -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[derive( + Debug, Clone, Default, PartialEq, Serialize, Deserialize, utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct PanicReceipt { pub id: ContractId, @@ -75,7 +83,9 @@ pub struct PanicReceipt { pub contract_id: Option, } -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[derive( + Debug, Clone, Default, PartialEq, Serialize, Deserialize, utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct RevertReceipt { pub id: ContractId, @@ -84,7 +94,9 @@ pub struct RevertReceipt { pub is: Word, } -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[derive( + Debug, Clone, Default, PartialEq, Serialize, Deserialize, utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct LogReceipt { pub id: ContractId, @@ -96,7 +108,9 @@ pub struct LogReceipt { pub is: Word, } -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[derive( + Debug, Clone, Default, PartialEq, Serialize, Deserialize, utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct LogDataReceipt { pub id: ContractId, @@ -110,7 +124,9 @@ pub struct LogDataReceipt { pub data: Option, } -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[derive( + Debug, Clone, Default, PartialEq, Serialize, Deserialize, utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct TransferReceipt { pub id: ContractId, @@ -121,7 +137,9 @@ pub struct TransferReceipt { pub is: Word, } -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[derive( + Debug, Clone, Default, PartialEq, Serialize, Deserialize, utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct TransferOutReceipt { pub id: ContractId, @@ -132,14 +150,18 @@ pub struct TransferOutReceipt { pub is: Word, } -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[derive( + Debug, Clone, Default, PartialEq, Serialize, Deserialize, utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct ScriptResultReceipt { pub result: ScriptExecutionResult, pub gas_used: Word, } -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[derive( + Debug, Clone, Default, PartialEq, Serialize, Deserialize, utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct MessageOutReceipt { pub sender: Address, @@ -151,7 +173,9 @@ pub struct MessageOutReceipt { pub data: Option, } -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[derive( + Debug, Clone, Default, PartialEq, Serialize, Deserialize, utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct MintReceipt { pub sub_id: Bytes32, @@ -161,7 +185,9 @@ pub struct MintReceipt { pub is: Word, } -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[derive( + Debug, Clone, Default, PartialEq, Serialize, Deserialize, utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct BurnReceipt { pub sub_id: Bytes32, @@ -368,7 +394,7 @@ impl From<&FuelCoreReceipt> for Receipt { } } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, utoipa::ToSchema)] pub enum ReceiptType { Call, Return, diff --git a/crates/domains/src/transactions/queryable.rs b/crates/domains/src/transactions/queryable.rs index 7ec200d6..aeacf487 100644 --- a/crates/domains/src/transactions/queryable.rs +++ b/crates/domains/src/transactions/queryable.rs @@ -31,7 +31,9 @@ pub enum Transactions { PublishedAt, } -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +#[derive( + Debug, Clone, Default, Serialize, Deserialize, PartialEq, utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct TransactionsQuery { pub tx_id: Option, diff --git a/crates/domains/src/transactions/types.rs b/crates/domains/src/transactions/types.rs index cc885586..3f12b683 100644 --- a/crates/domains/src/transactions/types.rs +++ b/crates/domains/src/transactions/types.rs @@ -5,7 +5,9 @@ use serde::{Deserialize, Serialize}; use crate::{inputs::types::*, outputs::types::*, receipts::types::*}; -#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +#[derive( + Debug, Default, Clone, PartialEq, Serialize, Deserialize, utoipa::ToSchema, +)] pub struct StorageSlot { pub key: HexData, pub value: HexData, @@ -26,7 +28,104 @@ impl From<&FuelCoreStorageSlot> for StorageSlot { } } -#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +#[derive( + Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq, Default, Hash, +)] +pub struct PolicyWrapper(pub FuelCorePolicies); + +impl utoipa::ToSchema for PolicyWrapper { + fn name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("FuelCorePolicies") + } +} + +impl utoipa::PartialSchema for PolicyWrapper { + fn schema() -> utoipa::openapi::RefOr { + utoipa::openapi::schema::ObjectBuilder::new() + .schema_type(utoipa::openapi::schema::Type::Array) + .title(Some("FuelCorePolicies")) + .description(Some("Array of u64 policy values used by the VM")) + .property( + "values", + utoipa::openapi::schema::ObjectBuilder::new() + .schema_type(utoipa::openapi::schema::Type::Integer) + .format(Some( + utoipa::openapi::schema::SchemaFormat::KnownFormat( + utoipa::openapi::KnownFormat::Int64, + ), + )) + .build(), + ) + .examples([Some(serde_json::json!([0, 0, 0, 0, 0]))]) + .build() + .into() + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq, Hash)] +pub struct FuelCoreUpgradePurposeWrapper(pub FuelCoreUpgradePurpose); + +impl From for FuelCoreUpgradePurposeWrapper { + fn from(purpose: FuelCoreUpgradePurpose) -> Self { + FuelCoreUpgradePurposeWrapper(purpose) + } +} + +impl From for FuelCoreUpgradePurpose { + fn from(wrapper: FuelCoreUpgradePurposeWrapper) -> Self { + wrapper.0 + } +} + +impl utoipa::ToSchema for FuelCoreUpgradePurposeWrapper { + fn name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("FuelCoreUpgradePurpose") + } +} + +impl utoipa::PartialSchema for FuelCoreUpgradePurposeWrapper { + fn schema() -> utoipa::openapi::RefOr { + // Create Object builders first + let consensus_params_obj = utoipa::openapi::schema::ObjectBuilder::new() + .schema_type(utoipa::openapi::schema::Type::Object) + .title(Some("ConsensusParameters")) + // ... other properties + .build(); + + let state_transition_obj = utoipa::openapi::schema::ObjectBuilder::new() + .schema_type(utoipa::openapi::schema::Type::Object) + .title(Some("StateTransition")) + // ... other properties + .build(); + + // Convert Objects to Schemas + let consensus_params = + utoipa::openapi::schema::Schema::Object(consensus_params_obj); + let state_transition = + utoipa::openapi::schema::Schema::Object(state_transition_obj); + + // Create a oneOf schema with both variants + let mut one_of = utoipa::openapi::schema::OneOf::new(); + + // Now we can add Schemas to the items + one_of + .items + .push(utoipa::openapi::RefOr::T(consensus_params)); + one_of + .items + .push(utoipa::openapi::RefOr::T(state_transition)); + + // Create the oneOf schema and return it + let schema = utoipa::openapi::schema::Schema::OneOf(one_of); + + // Return the Schema + utoipa::openapi::RefOr::T(schema) + } +} + +#[derive( + Debug, Default, Clone, PartialEq, Serialize, Deserialize, utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct Transaction { pub id: TxId, @@ -50,7 +149,7 @@ pub struct Transaction { pub mint_amount: Option, pub mint_asset_id: Option, pub mint_gas_price: Option, - pub policies: Option, + pub policies: Option, pub proof_set: Vec, pub raw_payload: HexData, pub receipts_root: Option, @@ -63,7 +162,7 @@ pub struct Transaction { pub subsection_index: Option, pub subsections_number: Option, pub tx_pointer: Option, - pub upgrade_purpose: Option, + pub upgrade_purpose: Option, pub witnesses: Vec, pub receipts: Vec, } @@ -393,7 +492,7 @@ impl Transaction { mint_amount: mint_amount.map(|amount| amount.into()), mint_asset_id, mint_gas_price: mint_gas_price.map(|amount| amount.into()), - policies, + policies: policies.map(PolicyWrapper), proof_set, raw_payload, receipts_root, @@ -406,7 +505,7 @@ impl Transaction { subsection_index, subsections_number, tx_pointer: Some(tx_pointer.into()), - upgrade_purpose, + upgrade_purpose: upgrade_purpose.map(FuelCoreUpgradePurposeWrapper), witnesses, receipts: receipts.iter().map(|r| r.to_owned().into()).collect(), } @@ -472,7 +571,7 @@ impl MockTransaction { mint_amount: None, mint_asset_id: None, mint_gas_price: None, - policies: Some(FuelCorePolicies::default()), + policies: Some(PolicyWrapper::default()), proof_set: vec![], raw_payload: HexData::default(), receipts_root: None, @@ -577,9 +676,12 @@ impl MockTransaction { receipts: Vec, ) -> Transaction { let mut tx = Self::base_transaction(TransactionType::Upgrade); - tx.upgrade_purpose = Some(FuelCoreUpgradePurpose::StateTransition { - root: FuelCoreBytes32::default(), - }); + tx.upgrade_purpose = Some( + FuelCoreUpgradePurpose::StateTransition { + root: FuelCoreBytes32::default(), + } + .into(), + ); tx.inputs = inputs; tx.outputs = outputs; tx.receipts = receipts; diff --git a/crates/domains/src/utxos/queryable.rs b/crates/domains/src/utxos/queryable.rs index a38aa917..abc9fa59 100644 --- a/crates/domains/src/utxos/queryable.rs +++ b/crates/domains/src/utxos/queryable.rs @@ -38,7 +38,9 @@ pub enum Utxos { PublishedAt, } -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +#[derive( + Debug, Clone, Default, Serialize, Deserialize, PartialEq, utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct UtxosQuery { pub tx_id: Option, diff --git a/crates/domains/src/utxos/types.rs b/crates/domains/src/utxos/types.rs index 87853e9e..a2d10872 100644 --- a/crates/domains/src/utxos/types.rs +++ b/crates/domains/src/utxos/types.rs @@ -1,7 +1,16 @@ use fuel_streams_types::primitives::*; use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +#[derive( + Debug, + Clone, + Default, + Serialize, + Deserialize, + PartialEq, + Eq, + utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct Utxo { pub utxo_id: UtxoId, diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml index 8e5b4d97..98bf6d88 100644 --- a/crates/store/Cargo.toml +++ b/crates/store/Cargo.toml @@ -31,6 +31,7 @@ tokio = { workspace = true, features = [ "test-util", ] } tracing.workspace = true +utoipa = { version = "5.3.1", features = ["actix_extras"] } [dev-dependencies] test-case.workspace = true diff --git a/crates/store/src/record/record_packet.rs b/crates/store/src/record/record_packet.rs index 99ae5485..44d749a4 100644 --- a/crates/store/src/record/record_packet.rs +++ b/crates/store/src/record/record_packet.rs @@ -20,7 +20,7 @@ pub trait PacketBuilder: Send + Sync + 'static { fn build_packets(opts: &Self::Opts) -> Vec; } -#[derive(Debug, Default, Clone, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct RecordPointer { pub block_height: BlockHeight, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml index b4873db5..faa26427 100644 --- a/crates/types/Cargo.toml +++ b/crates/types/Cargo.toml @@ -31,6 +31,7 @@ sqlx.workspace = true thiserror.workspace = true tokio.workspace = true tracing.workspace = true +utoipa = { version = "5.3.1", features = ["actix_extras"] } [dev-dependencies] pretty_assertions.workspace = true diff --git a/crates/types/src/primitives/amount.rs b/crates/types/src/primitives/amount.rs index 5a0622ca..40d0805f 100644 --- a/crates/types/src/primitives/amount.rs +++ b/crates/types/src/primitives/amount.rs @@ -1,4 +1,4 @@ -use crate::declare_integer_wrapper; +use crate::{declare_integer_wrapper, impl_utoipa_for_integer_wrapper}; #[derive(thiserror::Error, Debug, Clone, PartialEq)] pub enum AmountError { @@ -7,3 +7,10 @@ pub enum AmountError { } declare_integer_wrapper!(Amount, u64, AmountError); + +impl_utoipa_for_integer_wrapper!( + Amount, + "Amount in the blockchain", + 0, + u64::MAX as usize +); diff --git a/crates/types/src/primitives/block_header.rs b/crates/types/src/primitives/block_header.rs index 26bd3931..d2b560c8 100644 --- a/crates/types/src/primitives/block_header.rs +++ b/crates/types/src/primitives/block_header.rs @@ -1,3 +1,4 @@ +use chrono::Utc; use serde::{Deserialize, Serialize}; use wrapped_int::WrappedU32; @@ -26,8 +27,34 @@ impl Default for BlockTime { } } +impl utoipa::ToSchema for BlockTime { + fn name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("BlockTime") + } +} + +impl utoipa::PartialSchema for BlockTime { + fn schema() -> utoipa::openapi::RefOr { + utoipa::openapi::schema::ObjectBuilder::new() + .schema_type(utoipa::openapi::schema::Type::Integer) + .format(Some(utoipa::openapi::schema::SchemaFormat::Custom( + "tai64-timestamp".to_string(), + ))) + .description(Some( + "Block time as TAI64 format (convertible to/from Unix seconds)", + )) + .examples([Some(serde_json::json!(FuelCoreTai64::from_unix( + Utc::now().timestamp() + )))]) + .build() + .into() + } +} + // Header type -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +#[derive( + Debug, Clone, PartialEq, Serialize, Deserialize, Default, utoipa::ToSchema, +)] #[serde(rename_all = "camelCase")] pub struct BlockHeader { pub application_hash: Bytes32, @@ -76,7 +103,9 @@ impl From<&FuelCoreBlockHeader> for BlockHeader { } // BlockHeaderVersion enum -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +#[derive( + Debug, Clone, PartialEq, Serialize, Deserialize, Default, utoipa::ToSchema, +)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum BlockHeaderVersion { #[default] diff --git a/crates/types/src/primitives/block_height.rs b/crates/types/src/primitives/block_height.rs index a22482d1..287cb5b6 100644 --- a/crates/types/src/primitives/block_height.rs +++ b/crates/types/src/primitives/block_height.rs @@ -1,4 +1,8 @@ -use crate::{declare_integer_wrapper, fuel_core::FuelCoreBlockHeight}; +use crate::{ + declare_integer_wrapper, + fuel_core::FuelCoreBlockHeight, + impl_utoipa_for_integer_wrapper, +}; #[derive(thiserror::Error, Debug)] pub enum BlockHeightError { @@ -8,6 +12,13 @@ pub enum BlockHeightError { declare_integer_wrapper!(BlockHeight, u64, BlockHeightError); +impl_utoipa_for_integer_wrapper!( + BlockHeight, + "Block height in the blockchain", + 0, + u64::MAX as usize +); + impl From<&FuelCoreBlockHeight> for BlockHeight { fn from(value: &FuelCoreBlockHeight) -> Self { value.to_owned().into() diff --git a/crates/types/src/primitives/block_timestamp.rs b/crates/types/src/primitives/block_timestamp.rs index b66d223a..96f76b63 100644 --- a/crates/types/src/primitives/block_timestamp.rs +++ b/crates/types/src/primitives/block_timestamp.rs @@ -179,6 +179,26 @@ impl Default for BlockTimestamp { } } +impl utoipa::ToSchema for BlockTimestamp { + fn name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("BlockTimestamp") + } +} + +impl utoipa::PartialSchema for BlockTimestamp { + fn schema() -> utoipa::openapi::RefOr { + utoipa::openapi::schema::ObjectBuilder::new() + .schema_type(utoipa::openapi::schema::Type::Integer) + .format(Some(utoipa::openapi::schema::SchemaFormat::Custom( + "unix-timestamp".to_string(), + ))) + .description(Some("Block timestamp as Unix seconds since epoch")) + .examples([Some(serde_json::json!(Utc::now().timestamp()))]) + .build() + .into() + } +} + impl From<&super::BlockHeader> for BlockTimestamp { fn from(header: &super::BlockHeader) -> Self { Self::from_tai64(header.time.clone().into_inner()) diff --git a/crates/types/src/primitives/bytes.rs b/crates/types/src/primitives/bytes.rs index e81d69d8..30a3764b 100644 --- a/crates/types/src/primitives/bytes.rs +++ b/crates/types/src/primitives/bytes.rs @@ -7,6 +7,7 @@ use crate::{ generate_byte_type_wrapper, impl_bytes32_to_type, impl_from_type_to_bytes32, + impl_utoipa_for_byte_type_detailed, }; generate_byte_type_wrapper!(Address, fuel_types::Address, 32); @@ -22,6 +23,64 @@ generate_byte_type_wrapper!(Signature, fuel_types::Bytes64, 64); generate_byte_type_wrapper!(TxId, fuel_types::TxId, 32); generate_byte_type_wrapper!(HexData, LongBytes); +impl_utoipa_for_byte_type_detailed!( + Address, + 32, + "A 32-byte Fuel address with 0x prefix" +); + +impl_utoipa_for_byte_type_detailed!( + BlobId, + 32, + "A 32-byte Fuel blob id with 0x prefix" +); + +impl_utoipa_for_byte_type_detailed!(Salt, 32, "A 32-byte salt with 0x prefix"); + +impl_utoipa_for_byte_type_detailed!( + AssetId, + 32, + "A 32-byte asset identifier with 0x prefix" +); +impl_utoipa_for_byte_type_detailed!( + Bytes32, + 32, + "A 32-byte value with 0x prefix" +); +impl_utoipa_for_byte_type_detailed!( + ContractId, + 32, + "A 32-byte contract identifier with 0x prefix" +); +impl_utoipa_for_byte_type_detailed!( + TxId, + 32, + "A 32-byte transaction identifier with 0x prefix" +); + +impl_utoipa_for_byte_type_detailed!( + BlockId, + 32, + "A 32-byte block identifier with 0x prefix" +); + +impl_utoipa_for_byte_type_detailed!( + Signature, + 64, + "A 64-byte signature with 0x prefix" +); + +impl_utoipa_for_byte_type_detailed!( + HexData, + "Variable-length hexadecimal data with 0x prefix" +); + +impl_utoipa_for_byte_type_detailed!( + Nonce, + 32, + "A 32-byte Fuel nonce with 0x prefix" +); + impl TxId { pub fn random() -> Self { let mut rng = rand::rng(); diff --git a/crates/types/src/primitives/bytes_long.rs b/crates/types/src/primitives/bytes_long.rs index 958497ed..deb4dac8 100644 --- a/crates/types/src/primitives/bytes_long.rs +++ b/crates/types/src/primitives/bytes_long.rs @@ -7,6 +7,7 @@ serde::Serialize, serde::Deserialize, Default, + utoipa::ToSchema, )] pub struct LongBytes(pub Vec); diff --git a/crates/types/src/primitives/da_block_height.rs b/crates/types/src/primitives/da_block_height.rs index 0c5d99e8..c66c1352 100644 --- a/crates/types/src/primitives/da_block_height.rs +++ b/crates/types/src/primitives/da_block_height.rs @@ -1,6 +1,6 @@ use fuel_core_types::blockchain::primitives::DaBlockHeight as FuelCoreDaBlockHeight; -use crate::declare_integer_wrapper; +use crate::{declare_integer_wrapper, impl_utoipa_for_integer_wrapper}; #[derive(thiserror::Error, Debug)] pub enum DaBlockHeightError { @@ -10,6 +10,13 @@ pub enum DaBlockHeightError { declare_integer_wrapper!(DaBlockHeight, u64, DaBlockHeightError); +impl_utoipa_for_integer_wrapper!( + DaBlockHeight, + "Da Block height in the blockchain", + 0, + u64::MAX as usize +); + impl From for DaBlockHeight { fn from(value: FuelCoreDaBlockHeight) -> Self { value.0.into() diff --git a/crates/types/src/primitives/gas_amount.rs b/crates/types/src/primitives/gas_amount.rs index dab88b5b..215dc896 100644 --- a/crates/types/src/primitives/gas_amount.rs +++ b/crates/types/src/primitives/gas_amount.rs @@ -1,4 +1,4 @@ -use crate::declare_integer_wrapper; +use crate::{declare_integer_wrapper, impl_utoipa_for_integer_wrapper}; #[derive(thiserror::Error, Debug, Clone, PartialEq)] pub enum GasAmountError { @@ -7,3 +7,10 @@ pub enum GasAmountError { } declare_integer_wrapper!(GasAmount, u64, GasAmountError); + +impl_utoipa_for_integer_wrapper!( + GasAmount, + "Gas Amount in the blockchain", + 0, + u64::MAX as usize +); diff --git a/crates/types/src/primitives/mod.rs b/crates/types/src/primitives/mod.rs index 4acc374c..8b6dc538 100644 --- a/crates/types/src/primitives/mod.rs +++ b/crates/types/src/primitives/mod.rs @@ -8,6 +8,7 @@ pub mod common; pub mod da_block_height; pub mod gas_amount; pub mod identifier; +pub mod open_api; pub mod script_execution; pub mod tx_pointer; pub mod tx_status; diff --git a/crates/types/src/primitives/open_api.rs b/crates/types/src/primitives/open_api.rs new file mode 100644 index 00000000..9bde84f4 --- /dev/null +++ b/crates/types/src/primitives/open_api.rs @@ -0,0 +1,177 @@ +#[macro_export] +macro_rules! impl_utoipa_for_byte_type_detailed { + // Fixed-size type implementation with custom description + ($wrapper_type:ident, $byte_size:expr, $description:expr) => { + impl utoipa::ToSchema for $wrapper_type { + fn name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed(stringify!($wrapper_type)) + } + } + + impl utoipa::PartialSchema for $wrapper_type { + fn schema( + ) -> utoipa::openapi::RefOr { + utoipa::openapi::schema::ObjectBuilder::new() + .schema_type(utoipa::openapi::schema::Type::String) + .format(Some( + utoipa::openapi::schema::SchemaFormat::Custom( + "hex".to_string(), + ), + )) + .description(Some($description)) + .examples([Some(serde_json::json!(format!( + "0x{}", + "0".repeat($byte_size * 2) + )))]) + .pattern(Some(format!( + "^0x[0-9a-fA-F]{{{}}}$", + $byte_size * 2 + ))) + .min_length(Some(2 + $byte_size * 2)) + .max_length(Some(2 + $byte_size * 2)) + .build() + .into() + } + } + }; + + // Variable-size type implementation with custom description + ($wrapper_type:ident, $description:expr) => { + impl utoipa::ToSchema for $wrapper_type { + fn name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed(stringify!($wrapper_type)) + } + } + + impl utoipa::PartialSchema for $wrapper_type { + fn schema( + ) -> utoipa::openapi::RefOr { + utoipa::openapi::schema::ObjectBuilder::new() + .schema_type(utoipa::openapi::schema::Type::String) + .format(Some( + utoipa::openapi::schema::SchemaFormat::Custom( + "hex".to_string(), + ), + )) + .description(Some($description)) + .examples([Some(serde_json::json!("0x00"))]) + .pattern(Some("^0x[0-9a-fA-F]+$")) + .build() + .into() + } + } + }; +} + +#[macro_export] +macro_rules! impl_utoipa_for_integer_wrapper { + // Basic implementation with just type name and description + ($wrapper_type:ident, $description:expr) => { + impl utoipa::ToSchema for $wrapper_type { + fn name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed(stringify!($wrapper_type)) + } + } + + impl utoipa::PartialSchema for $wrapper_type { + fn schema( + ) -> utoipa::openapi::RefOr { + utoipa::openapi::schema::ObjectBuilder::new() + .schema_type(utoipa::openapi::schema::Type::Integer) + .format(Some(utoipa::openapi::schema::SchemaFormat::Int64)) + .description(Some($description)) + .examples([Some(serde_json::json!(0))]) + .build() + .into() + } + } + }; + + // Implementation with min/max values and description + ($wrapper_type:ident, $description:expr, $min:expr, $max:expr) => { + impl utoipa::ToSchema for $wrapper_type { + fn name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed(stringify!($wrapper_type)) + } + } + + impl utoipa::PartialSchema for $wrapper_type { + fn schema( + ) -> utoipa::openapi::RefOr { + utoipa::openapi::schema::ObjectBuilder::new() + .schema_type(utoipa::openapi::schema::Type::Integer) + .format(Some( + utoipa::openapi::schema::SchemaFormat::KnownFormat( + utoipa::openapi::KnownFormat::Int64, + ), + )) + .description(Some($description)) + .examples([Some(serde_json::json!(0))]) + .minimum(Some(utoipa::Number::UInt($min))) + .maximum(Some(utoipa::Number::UInt($max))) + .build() + .into() + } + } + }; + + // Implementation with custom format (u32, i32, etc.) + ($wrapper_type:ident, $description:expr, $format:expr) => { + impl utoipa::ToSchema for $wrapper_type { + fn name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed(stringify!($wrapper_type)) + } + } + + impl utoipa::PartialSchema for $wrapper_type { + fn schema( + ) -> utoipa::openapi::RefOr { + utoipa::openapi::schema::ObjectBuilder::new() + .schema_type(utoipa::openapi::schema::Type::Integer) + .format(Some( + utoipa::openapi::schema::SchemaFormat::Custom( + $format.to_string(), + ), + )) + .description(Some($description)) + .examples([Some(serde_json::json!(0))]) + .build() + .into() + } + } + }; + + // Implementation with custom format and min/max values + ( + $wrapper_type:ident, + $description:expr, + $format:expr, + $min:expr, + $max:expr + ) => { + impl utoipa::ToSchema for $wrapper_type { + fn name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed(stringify!($wrapper_type)) + } + } + + impl utoipa::PartialSchema for $wrapper_type { + fn schema( + ) -> utoipa::openapi::RefOr { + utoipa::openapi::schema::ObjectBuilder::new() + .schema_type(utoipa::openapi::schema::Type::Integer) + .format(Some( + utoipa::openapi::schema::SchemaFormat::Custom( + $format.to_string(), + ), + )) + .description(Some($description)) + .examples([Some(serde_json::json!(0))]) + .minimum(Some(serde_json::json!($min))) + .maximum(Some(serde_json::json!($max))) + .build() + .into() + } + } + }; +} diff --git a/crates/types/src/primitives/script_execution.rs b/crates/types/src/primitives/script_execution.rs index 9d1f0a80..6b644771 100644 --- a/crates/types/src/primitives/script_execution.rs +++ b/crates/types/src/primitives/script_execution.rs @@ -9,6 +9,109 @@ pub struct PanicInstruction { pub reason: PanicReason, pub instruction: RawInstruction, } + +impl utoipa::ToSchema for PanicInstruction { + fn name() -> std::borrow::Cow<'static, str> { + std::borrow::Cow::Borrowed("PanicInstruction") + } +} + +impl utoipa::PartialSchema for PanicInstruction { + fn schema() -> utoipa::openapi::RefOr { + utoipa::openapi::schema::ObjectBuilder::new() + .title(Some("PanicInstruction")) + .description(Some("Instruction that caused a panic in the VM")) + .property( + "reason", + utoipa::openapi::schema::ObjectBuilder::new() + .schema_type(utoipa::openapi::schema::Type::String) + .enum_values(Some([ + "UnknownPanicReason", + "Revert", + "OutOfGas", + "TransactionValidity", + "MemoryOverflow", + "ArithmeticOverflow", + "ContractNotFound", + "MemoryOwnership", + "NotEnoughBalance", + "ExpectedInternalContext", + "AssetIdNotFound", + "InputNotFound", + "OutputNotFound", + "WitnessNotFound", + "TransactionMaturity", + "InvalidMetadataIdentifier", + "MalformedCallStructure", + "ReservedRegisterNotWritable", + "InvalidFlags", + "InvalidImmediateValue", + "ExpectedCoinInput", + "EcalError", + "MemoryWriteOverlap", + "ContractNotInInputs", + "InternalBalanceOverflow", + "ContractMaxSize", + "ExpectedUnallocatedStack", + "MaxStaticContractsReached", + "TransferAmountCannotBeZero", + "ExpectedOutputVariable", + "ExpectedParentInternalContext", + "PredicateReturnedNonOne", + "ContractIdAlreadyDeployed", + "ContractMismatch", + "MessageDataTooLong", + "ArithmeticError", + "ContractInstructionNotAllowed", + "TransferZeroCoins", + "InvalidInstruction", + "MemoryNotExecutable", + "PolicyIsNotSet", + "PolicyNotFound", + "TooManyReceipts", + "BalanceOverflow", + "InvalidBlockHeight", + "TooManySlots", + "ExpectedNestedCaller", + "MemoryGrowthOverlap", + "UninitializedMemoryAccess", + "OverridingConsensusParameters", + "UnknownStateTransactionBytecodeRoot", + "OverridingStateTransactionBytecode", + "BytecodeAlreadyUploaded", + "ThePartIsNotSequentiallyConnected", + "BlobNotFound", + "BlobIdAlreadyUploaded", + "GasCostNotDefined", + "UnsupportedCurveId", + "UnsupportedOperationType", + "InvalidEllipticCurvePoint", + "InputContractDoesNotExist", + ])) + .description(Some("Reason for VM panic")) + .build(), + ) + .property( + "instruction", + utoipa::openapi::schema::ObjectBuilder::new() + .schema_type(utoipa::openapi::schema::Type::Integer) + .format(Some( + utoipa::openapi::schema::SchemaFormat::KnownFormat( + utoipa::openapi::KnownFormat::Int32, + ), + )) + .description(Some( + "Raw instruction that caused the panic (u32)", + )) + .build(), + ) + .required("reason") + .required("instruction") + .build() + .into() + } +} + impl From for PanicInstruction { fn from(value: FuelCorePanicInstruction) -> Self { Self { @@ -28,6 +131,7 @@ impl From for PanicInstruction { Default, serde::Serialize, serde::Deserialize, + utoipa::ToSchema, )] #[repr(u64)] pub enum ScriptExecutionResult { diff --git a/crates/types/src/primitives/tx_pointer.rs b/crates/types/src/primitives/tx_pointer.rs index 24317571..9a8870be 100644 --- a/crates/types/src/primitives/tx_pointer.rs +++ b/crates/types/src/primitives/tx_pointer.rs @@ -13,6 +13,7 @@ use crate::fuel_core::*; Hash, serde::Deserialize, serde::Serialize, + utoipa::ToSchema, )] pub struct TxPointer { block_height: BlockHeight, diff --git a/crates/types/src/primitives/tx_status.rs b/crates/types/src/primitives/tx_status.rs index 37083a7d..0f33b223 100644 --- a/crates/types/src/primitives/tx_status.rs +++ b/crates/types/src/primitives/tx_status.rs @@ -7,7 +7,9 @@ use crate::fuel_core::{ FuelCoreTransactionStatus, }; -#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +#[derive( + Debug, Default, Clone, PartialEq, Serialize, Deserialize, utoipa::ToSchema, +)] #[serde(rename_all = "snake_case")] pub enum TransactionStatus { Failed, diff --git a/crates/types/src/primitives/tx_type.rs b/crates/types/src/primitives/tx_type.rs index 910aad72..4c8f5285 100644 --- a/crates/types/src/primitives/tx_type.rs +++ b/crates/types/src/primitives/tx_type.rs @@ -2,7 +2,9 @@ use serde::{Deserialize, Serialize}; use crate::fuel_core::FuelCoreTransaction; -#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +#[derive( + Debug, Default, Clone, PartialEq, Serialize, Deserialize, utoipa::ToSchema, +)] #[serde(rename_all = "snake_case")] pub enum TransactionType { #[default] diff --git a/crates/types/src/primitives/utxo_id.rs b/crates/types/src/primitives/utxo_id.rs index 128df4c9..f2d8bca4 100644 --- a/crates/types/src/primitives/utxo_id.rs +++ b/crates/types/src/primitives/utxo_id.rs @@ -10,6 +10,7 @@ use crate::fuel_core::*; Hash, serde::Serialize, serde::Deserialize, + utoipa::ToSchema, )] pub struct UtxoId { pub tx_id: Bytes32, diff --git a/crates/types/src/primitives/word.rs b/crates/types/src/primitives/word.rs index 05f02664..86d37582 100644 --- a/crates/types/src/primitives/word.rs +++ b/crates/types/src/primitives/word.rs @@ -1,4 +1,4 @@ -use crate::declare_integer_wrapper; +use crate::{declare_integer_wrapper, impl_utoipa_for_integer_wrapper}; #[derive(thiserror::Error, Debug)] pub enum WordError { @@ -7,3 +7,10 @@ pub enum WordError { } declare_integer_wrapper!(Word, u64, WordError); + +impl_utoipa_for_integer_wrapper!( + Word, + "A word in the blockchain", + 0, + u64::MAX as usize +); diff --git a/crates/types/src/primitives/wrapped_int.rs b/crates/types/src/primitives/wrapped_int.rs index 0ff86261..dd2a2dd1 100644 --- a/crates/types/src/primitives/wrapped_int.rs +++ b/crates/types/src/primitives/wrapped_int.rs @@ -1,3 +1,5 @@ +use crate::impl_utoipa_for_integer_wrapper; + #[macro_export] macro_rules! impl_conversions { ($name:ident, $inner_type:ty, $($t:ty),*) => { @@ -280,3 +282,17 @@ pub enum WrappedIntError { declare_integer_wrapper!(WrappedU32, u32, WrappedIntError); declare_integer_wrapper!(WrappedU64, u64, WrappedIntError); + +impl_utoipa_for_integer_wrapper!( + WrappedU32, + "Wrapped u32 in the blockchain", + 0, + u32::MAX as usize +); + +impl_utoipa_for_integer_wrapper!( + WrappedU64, + "Wrapped u64 in the blockchain", + 0, + u64::MAX as usize +); diff --git a/scripts/run_api.sh b/scripts/run_api.sh index 69bbdcc0..4e7165ac 100755 --- a/scripts/run_api.sh +++ b/scripts/run_api.sh @@ -71,7 +71,7 @@ echo -e "==========================================\n" # Define common arguments COMMON_ARGS=( - "--port" "${PORT:-9003}" + "--port" "${PORT:-9004}" ) # Execute based on mode diff --git a/services/api/Cargo.toml b/services/api/Cargo.toml index c9b6e4b1..05d0432d 100644 --- a/services/api/Cargo.toml +++ b/services/api/Cargo.toml @@ -45,6 +45,8 @@ time = { version = "0.3", features = ["serde"] } tokio.workspace = true tracing.workspace = true tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } +utoipa = { version = "5.3.1", features = ["actix_extras"] } +utoipa-swagger-ui = { version = "9.0.0", features = ["actix-web"] } validator = { version = "0.19", features = ["derive"] } # in an individual package Cargo.toml diff --git a/services/api/src/main.rs b/services/api/src/main.rs index 1e14edba..a373f193 100644 --- a/services/api/src/main.rs +++ b/services/api/src/main.rs @@ -29,6 +29,7 @@ async fn main() -> anyhow::Result<()> { .build()?; let server_handle = server.handle(); let server_task = spawn_web_server(server).await; + tracing::info!("Server started on port {}", config.api.port); let _ = tokio::join!(server_task); // Await the Actix server shutdown diff --git a/services/api/src/server/handlers/accounts.rs b/services/api/src/server/handlers/accounts.rs index 5c20e42a..a15a047e 100644 --- a/services/api/src/server/handlers/accounts.rs +++ b/services/api/src/server/handlers/accounts.rs @@ -1,4 +1,16 @@ use actix_web::{web, HttpRequest, HttpResponse}; +use fuel_streams_core::types::{ + Address, + AssetId, + BlockHeight, + ContractId, + HexData, + InputType, + OutputType, + TransactionStatus, + TransactionType, + TxId, +}; use fuel_streams_domains::{ inputs::queryable::InputsQuery, outputs::queryable::OutputsQuery, @@ -11,6 +23,35 @@ use fuel_web_utils::api_key::ApiKey; use super::{Error, GetDataResponse}; use crate::server::state::ServerState; +#[utoipa::path( + get, + path = "/accounts/{address}/transactions", + tag = "accounts", + params( + // Path parameter + ("address" = String, Path, description = "Account address"), + // TransactionsQuery fields + ("txId" = Option, Query, description = "Filter by transaction ID"), + ("txIndex" = Option, Query, description = "Filter by transaction index"), + ("txStatus" = Option, Query, description = "Filter by transaction status"), + ("type" = Option, Query, description = "Filter by transaction type"), + ("blockHeight" = Option, Query, description = "Filter by block height"), + // Flattened QueryPagination fields + ("after" = Option, Query, description = "Return transactions after this height"), + ("before" = Option, Query, description = "Return transactions before this height"), + ("first" = Option, Query, description = "Limit results, sorted by ascending block height", maximum = 100), + ("last" = Option, Query, description = "Limit results, sorted by descending block height", maximum = 100) + ), + responses( + (status = 200, description = "Successfully retrieved account transactions", body = GetDataResponse), + (status = 400, description = "Invalid query parameters", body = String), + (status = 404, description = "Account not found", body = String), + (status = 500, description = "Internal server error", body = String) + ), + security( + ("api_key" = []) + ) +)] pub async fn get_accounts_transactions( req: HttpRequest, address: web::Path, @@ -29,6 +70,40 @@ pub async fn get_accounts_transactions( Ok(HttpResponse::Ok().json(response)) } +#[utoipa::path( + get, + path = "/accounts/{address}/inputs", + tag = "accounts", + params( + // Path parameter + ("address" = String, Path, description = "Account address"), + // InputsQuery fields + ("txId" = Option, Query, description = "Filter by transaction ID"), + ("txIndex" = Option, Query, description = "Filter by transaction index"), + ("inputIndex" = Option, Query, description = "Filter by input index"), + ("inputType" = Option, Query, description = "Filter by input type"), + ("blockHeight" = Option, Query, description = "Filter by block height"), + ("ownerId" = Option
, Query, description = "Filter by owner ID (for coin inputs)"), + ("assetId" = Option, Query, description = "Filter by asset ID (for coin inputs)"), + ("contractId" = Option, Query, description = "Filter by contract ID (for contract inputs)"), + ("senderAddress" = Option
, Query, description = "Filter by sender address (for message inputs)"), + ("recipientAddress" = Option
, Query, description = "Filter by recipient address (for message inputs)"), + // Flattened QueryPagination fields + ("after" = Option, Query, description = "Return inputs after this height"), + ("before" = Option, Query, description = "Return inputs before this height"), + ("first" = Option, Query, description = "Limit results, sorted by ascending block height", maximum = 100), + ("last" = Option, Query, description = "Limit results, sorted by descending block height", maximum = 100) + ), + responses( + (status = 200, description = "Successfully retrieved account inputs", body = GetDataResponse), + (status = 400, description = "Invalid query parameters", body = String), + (status = 404, description = "Account not found", body = String), + (status = 500, description = "Internal server error", body = String) + ), + security( + ("api_key" = []) + ) +)] pub async fn get_accounts_inputs( req: HttpRequest, address: web::Path, @@ -47,6 +122,38 @@ pub async fn get_accounts_inputs( Ok(HttpResponse::Ok().json(response)) } +#[utoipa::path( + get, + path = "/accounts/{address}/outputs", + tag = "accounts", + params( + // Path parameter + ("address" = String, Path, description = "Account address"), + // OutputsQuery fields + ("txId" = Option, Query, description = "Filter by transaction ID"), + ("txIndex" = Option, Query, description = "Filter by transaction index"), + ("outputIndex" = Option, Query, description = "Filter by output index"), + ("outputType" = Option, Query, description = "Filter by output type"), + ("blockHeight" = Option, Query, description = "Filter by block height"), + ("toAddress" = Option
, Query, description = "Filter by recipient address (for coin, change, and variable outputs)"), + ("assetId" = Option, Query, description = "Filter by asset ID (for coin, change, and variable outputs)"), + ("contractId" = Option, Query, description = "Filter by contract ID (for contract and contract_created outputs)"), + // Flattened QueryPagination fields + ("after" = Option, Query, description = "Return outputs after this height"), + ("before" = Option, Query, description = "Return outputs before this height"), + ("first" = Option, Query, description = "Limit results, sorted by ascending block height", maximum = 100), + ("last" = Option, Query, description = "Limit results, sorted by descending block height", maximum = 100) + ), + responses( + (status = 200, description = "Successfully retrieved account outputs", body = GetDataResponse), + (status = 400, description = "Invalid query parameters", body = String), + (status = 404, description = "Account not found", body = String), + (status = 500, description = "Internal server error", body = String) + ), + security( + ("api_key" = []) + ) +)] pub async fn get_accounts_outputs( req: HttpRequest, address: web::Path, @@ -65,6 +172,37 @@ pub async fn get_accounts_outputs( Ok(HttpResponse::Ok().json(response)) } +#[utoipa::path( + get, + path = "/accounts/{address}/utxos", + tag = "accounts", + params( + // Path parameter + ("address" = String, Path, description = "Account address"), + // UtxosQuery fields + ("txId" = Option, Query, description = "Filter by transaction ID"), + ("txIndex" = Option, Query, description = "Filter by transaction index"), + ("inputIndex" = Option, Query, description = "Filter by input index"), + ("utxoType" = Option, Query, description = "Filter by UTXO type"), + ("blockHeight" = Option, Query, description = "Filter by block height"), + ("utxoId" = Option, Query, description = "Filter by UTXO ID"), + ("contractId" = Option, Query, description = "Filter by contract ID (for contract UTXOs)"), + // Flattened QueryPagination fields + ("after" = Option, Query, description = "Return UTXOs after this height"), + ("before" = Option, Query, description = "Return UTXOs before this height"), + ("first" = Option, Query, description = "Limit results, sorted by ascending block height", maximum = 100), + ("last" = Option, Query, description = "Limit results, sorted by descending block height", maximum = 100) + ), + responses( + (status = 200, description = "Successfully retrieved account UTXOs", body = GetDataResponse), + (status = 400, description = "Invalid query parameters", body = String), + (status = 404, description = "Account not found", body = String), + (status = 500, description = "Internal server error", body = String) + ), + security( + ("api_key" = []) + ) +)] pub async fn get_accounts_utxos( req: HttpRequest, address: web::Path, diff --git a/services/api/src/server/handlers/blocks.rs b/services/api/src/server/handlers/blocks.rs index 2d9b3d28..c3561644 100644 --- a/services/api/src/server/handlers/blocks.rs +++ b/services/api/src/server/handlers/blocks.rs @@ -1,6 +1,20 @@ use actix_web::{web, HttpRequest, HttpResponse}; +use fuel_streams_core::types::{ + Address, + AssetId, + BlockHeight, + BlockTimestamp, + Bytes32, + ContractId, + InputType, + OutputType, + ReceiptType, + TransactionStatus, + TransactionType, + TxId, +}; use fuel_streams_domains::{ - blocks::queryable::BlocksQuery, + blocks::queryable::{BlocksQuery, TimeRange}, inputs::queryable::InputsQuery, outputs::queryable::OutputsQuery, queryable::{Queryable, ValidatedQuery}, @@ -12,6 +26,31 @@ use fuel_web_utils::api_key::ApiKey; use super::{Error, GetDataResponse}; use crate::server::state::ServerState; +#[utoipa::path( + get, + path = "/blocks", + tag = "blocks", + params( + // BlocksQuery fields + ("producer" = Option
, Query, description = "Filter by block producer address"), + ("height" = Option, Query, description = "Filter by block height"), + ("timestamp" = Option, Query, description = "Filter by timestamp"), + ("timeRange" = Option, Query, description = "Filter by time range"), + // Flattened QueryPagination fields + ("after" = Option, Query, description = "Return blocks after this height"), + ("before" = Option, Query, description = "Return blocks before this height"), + ("first" = Option, Query, description = "Limit results, sorted by ascending block height", maximum = 100), + ("last" = Option, Query, description = "Limit results, sorted by descending block height", maximum = 100), + ), + responses( + (status = 200, description = "Successfully retrieved blocks", body = GetDataResponse), + (status = 400, description = "Invalid query parameters", body = String), + (status = 500, description = "Internal server error", body = String) + ), + security( + ("api_key" = []) + ) +)] pub async fn get_blocks( req: HttpRequest, req_query: ValidatedQuery, @@ -27,6 +66,34 @@ pub async fn get_blocks( Ok(HttpResponse::Ok().json(response)) } +#[utoipa::path( + get, + path = "/blocks/{height}/transactions", + tag = "blocks", + params( + // Path parameter + ("height" = BlockHeight, Path, description = "Block height"), + // TransactionsQuery fields + ("txId" = Option, Query, description = "Filter by transaction ID"), + ("txIndex" = Option, Query, description = "Filter by transaction index"), + ("txStatus" = Option, Query, description = "Filter by transaction status"), + ("type" = Option, Query, description = "Filter by transaction type"), + // Flattened QueryPagination fields + ("after" = Option, Query, description = "Return transactions after this height"), + ("before" = Option, Query, description = "Return transactions before this height"), + ("first" = Option, Query, description = "Limit results, sorted by ascending block height", maximum = 100), + ("last" = Option, Query, description = "Limit results, sorted by descending block height", maximum = 100) + ), + responses( + (status = 200, description = "Successfully retrieved block transactions", body = GetDataResponse), + (status = 400, description = "Invalid query parameters", body = String), + (status = 404, description = "Block not found", body = String), + (status = 500, description = "Internal server error", body = String) + ), + security( + ("api_key" = []) + ) +)] pub async fn get_block_transactions( req: HttpRequest, height: web::Path, @@ -45,6 +112,42 @@ pub async fn get_block_transactions( Ok(HttpResponse::Ok().json(response)) } +#[utoipa::path( + get, + path = "/blocks/{height}/receipts", + tag = "blocks", + params( + // Path parameter + ("height" = BlockHeight, Path, description = "Block height"), + // ReceiptsQuery fields + ("txId" = Option, Query, description = "Filter by transaction ID"), + ("txIndex" = Option, Query, description = "Filter by transaction index"), + ("receiptIndex" = Option, Query, description = "Filter by receipt index"), + ("receiptType" = Option, Query, description = "Filter by receipt type"), + ("from" = Option, Query, description = "Filter by source contract ID"), + ("to" = Option, Query, description = "Filter by destination contract ID"), + ("contract" = Option, Query, description = "Filter by contract ID"), + ("asset" = Option, Query, description = "Filter by asset ID"), + ("sender" = Option
, Query, description = "Filter by sender address"), + ("recipient" = Option
, Query, description = "Filter by recipient address"), + ("subId" = Option, Query, description = "Filter by sub ID"), + ("address" = Option
, Query, description = "Filter by address (for accounts)"), + // Flattened QueryPagination fields + ("after" = Option, Query, description = "Return receipts after this height"), + ("before" = Option, Query, description = "Return receipts before this height"), + ("first" = Option, Query, description = "Limit results, sorted by ascending block height", maximum = 100), + ("last" = Option, Query, description = "Limit results, sorted by descending block height", maximum = 100) + ), + responses( + (status = 200, description = "Successfully retrieved block receipts", body = GetDataResponse), + (status = 400, description = "Invalid query parameters", body = String), + (status = 404, description = "Block not found", body = String), + (status = 500, description = "Internal server error", body = String) + ), + security( + ("api_key" = []) + ) +)] pub async fn get_block_receipts( req: HttpRequest, height: web::Path, @@ -63,6 +166,39 @@ pub async fn get_block_receipts( Ok(HttpResponse::Ok().json(response)) } +#[utoipa::path( + get, + path = "/blocks/{height}/inputs", + tag = "blocks", + params( + // Path parameter + ("height" = BlockHeight, Path, description = "Block height"), + // InputsQuery fields + ("txId" = Option, Query, description = "Filter by transaction ID"), + ("txIndex" = Option, Query, description = "Filter by transaction index"), + ("inputIndex" = Option, Query, description = "Filter by input index"), + ("inputType" = Option, Query, description = "Filter by input type"), + ("ownerId" = Option
, Query, description = "Filter by owner ID (for coin inputs)"), + ("assetId" = Option, Query, description = "Filter by asset ID (for coin inputs)"), + ("contractId" = Option, Query, description = "Filter by contract ID (for contract inputs)"), + ("senderAddress" = Option
, Query, description = "Filter by sender address (for message inputs)"), + ("recipientAddress" = Option
, Query, description = "Filter by recipient address (for message inputs)"), + // Flattened QueryPagination fields + ("after" = Option, Query, description = "Return inputs after this height"), + ("before" = Option, Query, description = "Return inputs before this height"), + ("first" = Option, Query, description = "Limit results, sorted by ascending block height", maximum = 100), + ("last" = Option, Query, description = "Limit results, sorted by descending block height", maximum = 100) + ), + responses( + (status = 200, description = "Successfully retrieved block inputs", body = GetDataResponse), + (status = 400, description = "Invalid query parameters", body = String), + (status = 404, description = "Block not found", body = String), + (status = 500, description = "Internal server error", body = String) + ), + security( + ("api_key" = []) + ) +)] pub async fn get_block_inputs( req: HttpRequest, height: web::Path, @@ -81,6 +217,38 @@ pub async fn get_block_inputs( Ok(HttpResponse::Ok().json(response)) } +#[utoipa::path( + get, + path = "/blocks/{height}/outputs", + tag = "blocks", + params( + // Path parameter + ("height" = BlockHeight, Path, description = "Block height"), + // OutputsQuery fields + ("txId" = Option, Query, description = "Filter by transaction ID"), + ("txIndex" = Option, Query, description = "Filter by transaction index"), + ("outputIndex" = Option, Query, description = "Filter by output index"), + ("outputType" = Option, Query, description = "Filter by output type"), + ("toAddress" = Option
, Query, description = "Filter by recipient address (for coin, change, and variable outputs)"), + ("assetId" = Option, Query, description = "Filter by asset ID (for coin, change, and variable outputs)"), + ("contractId" = Option, Query, description = "Filter by contract ID (for contract and contract_created outputs)"), + ("address" = Option
, Query, description = "Filter by address (for accounts)"), + // Flattened QueryPagination fields + ("after" = Option, Query, description = "Return outputs after this height"), + ("before" = Option, Query, description = "Return outputs before this height"), + ("first" = Option, Query, description = "Limit results, sorted by ascending block height", maximum = 100), + ("last" = Option, Query, description = "Limit results, sorted by descending block height", maximum = 100) + ), + responses( + (status = 200, description = "Successfully retrieved block outputs", body = GetDataResponse), + (status = 400, description = "Invalid query parameters", body = String), + (status = 404, description = "Block not found", body = String), + (status = 500, description = "Internal server error", body = String) + ), + security( + ("api_key" = []) + ) +)] pub async fn get_block_outputs( req: HttpRequest, height: web::Path, diff --git a/services/api/src/server/handlers/contracts.rs b/services/api/src/server/handlers/contracts.rs index 6eaad232..b6cccc01 100644 --- a/services/api/src/server/handlers/contracts.rs +++ b/services/api/src/server/handlers/contracts.rs @@ -1,4 +1,15 @@ use actix_web::{web, HttpRequest, HttpResponse}; +use fuel_streams_core::types::{ + Address, + AssetId, + BlockHeight, + HexData, + InputType, + OutputType, + TransactionStatus, + TransactionType, + TxId, +}; use fuel_streams_domains::{ inputs::queryable::InputsQuery, outputs::queryable::OutputsQuery, @@ -11,6 +22,35 @@ use fuel_web_utils::api_key::ApiKey; use super::{Error, GetDataResponse}; use crate::server::state::ServerState; +#[utoipa::path( + get, + path = "/contracts/{contractId}/transactions", + tag = "contracts", + params( + // Path parameter + ("contractId" = String, Path, description = "Contract ID"), + // TransactionsQuery fields + ("txId" = Option, Query, description = "Filter by transaction ID"), + ("txIndex" = Option, Query, description = "Filter by transaction index"), + ("txStatus" = Option, Query, description = "Filter by transaction status"), + ("type" = Option, Query, description = "Filter by transaction type"), + ("blockHeight" = Option, Query, description = "Filter by block height"), + // Flattened QueryPagination fields + ("after" = Option, Query, description = "Return transactions after this height"), + ("before" = Option, Query, description = "Return transactions before this height"), + ("first" = Option, Query, description = "Limit results, sorted by ascending block height", maximum = 100), + ("last" = Option, Query, description = "Limit results, sorted by descending block height", maximum = 100) + ), + responses( + (status = 200, description = "Successfully retrieved contract transactions", body = GetDataResponse), + (status = 400, description = "Invalid query parameters", body = String), + (status = 404, description = "Contract not found", body = String), + (status = 500, description = "Internal server error", body = String) + ), + security( + ("api_key" = []) + ) +)] pub async fn get_contracts_transactions( req: HttpRequest, contract_id: web::Path, @@ -29,6 +69,39 @@ pub async fn get_contracts_transactions( Ok(HttpResponse::Ok().json(response)) } +#[utoipa::path( + get, + path = "/contracts/{contractId}/inputs", + tag = "contracts", + params( + // Path parameter + ("contractId" = String, Path, description = "Contract ID"), + // InputsQuery fields + ("txId" = Option, Query, description = "Filter by transaction ID"), + ("txIndex" = Option, Query, description = "Filter by transaction index"), + ("inputIndex" = Option, Query, description = "Filter by input index"), + ("inputType" = Option, Query, description = "Filter by input type"), + ("blockHeight" = Option, Query, description = "Filter by block height"), + ("ownerId" = Option
, Query, description = "Filter by owner ID (for coin inputs)"), + ("assetId" = Option, Query, description = "Filter by asset ID (for coin inputs)"), + ("senderAddress" = Option
, Query, description = "Filter by sender address (for message inputs)"), + ("recipientAddress" = Option
, Query, description = "Filter by recipient address (for message inputs)"), + // Flattened QueryPagination fields + ("after" = Option, Query, description = "Return inputs after this height"), + ("before" = Option, Query, description = "Return inputs before this height"), + ("first" = Option, Query, description = "Limit results, sorted by ascending block height", maximum = 100), + ("last" = Option, Query, description = "Limit results, sorted by descending block height", maximum = 100) + ), + responses( + (status = 200, description = "Successfully retrieved contract inputs", body = GetDataResponse), + (status = 400, description = "Invalid query parameters", body = String), + (status = 404, description = "Contract not found", body = String), + (status = 500, description = "Internal server error", body = String) + ), + security( + ("api_key" = []) + ) +)] pub async fn get_contracts_inputs( req: HttpRequest, contract_id: web::Path, @@ -47,6 +120,37 @@ pub async fn get_contracts_inputs( Ok(HttpResponse::Ok().json(response)) } +#[utoipa::path( + get, + path = "/contracts/{contractId}/outputs", + tag = "contracts", + params( + // Path parameter + ("contractId" = String, Path, description = "Contract ID"), + // OutputsQuery fields + ("txId" = Option, Query, description = "Filter by transaction ID"), + ("txIndex" = Option, Query, description = "Filter by transaction index"), + ("outputIndex" = Option, Query, description = "Filter by output index"), + ("outputType" = Option, Query, description = "Filter by output type"), + ("blockHeight" = Option, Query, description = "Filter by block height"), + ("toAddress" = Option
, Query, description = "Filter by recipient address (for coin, change, and variable outputs)"), + ("assetId" = Option, Query, description = "Filter by asset ID (for coin, change, and variable outputs)"), + // Flattened QueryPagination fields + ("after" = Option, Query, description = "Return outputs after this height"), + ("before" = Option, Query, description = "Return outputs before this height"), + ("first" = Option, Query, description = "Limit results, sorted by ascending block height", maximum = 100), + ("last" = Option, Query, description = "Limit results, sorted by descending block height", maximum = 100) + ), + responses( + (status = 200, description = "Successfully retrieved contract outputs", body = GetDataResponse), + (status = 400, description = "Invalid query parameters", body = String), + (status = 404, description = "Contract not found", body = String), + (status = 500, description = "Internal server error", body = String) + ), + security( + ("api_key" = []) + ) +)] pub async fn get_contracts_outputs( req: HttpRequest, contract_id: web::Path, @@ -65,6 +169,36 @@ pub async fn get_contracts_outputs( Ok(HttpResponse::Ok().json(response)) } +#[utoipa::path( + get, + path = "/contracts/{contractId}/utxos", + tag = "contracts", + params( + // Path parameter + ("contractId" = String, Path, description = "Contract ID"), + // UtxosQuery fields + ("txId" = Option, Query, description = "Filter by transaction ID"), + ("txIndex" = Option, Query, description = "Filter by transaction index"), + ("inputIndex" = Option, Query, description = "Filter by input index"), + ("utxoType" = Option, Query, description = "Filter by UTXO type"), + ("blockHeight" = Option, Query, description = "Filter by block height"), + ("utxoId" = Option, Query, description = "Filter by UTXO ID"), + // Flattened QueryPagination fields + ("after" = Option, Query, description = "Return UTXOs after this height"), + ("before" = Option, Query, description = "Return UTXOs before this height"), + ("first" = Option, Query, description = "Limit results, sorted by ascending block height", maximum = 100), + ("last" = Option, Query, description = "Limit results, sorted by descending block height", maximum = 100) + ), + responses( + (status = 200, description = "Successfully retrieved contract UTXOs", body = GetDataResponse), + (status = 400, description = "Invalid query parameters", body = String), + (status = 404, description = "Contract not found", body = String), + (status = 500, description = "Internal server error", body = String) + ), + security( + ("api_key" = []) + ) +)] pub async fn get_contracts_utxos( req: HttpRequest, contract_id: web::Path, diff --git a/services/api/src/server/handlers/inputs.rs b/services/api/src/server/handlers/inputs.rs index 8f3e8a30..4f9fae47 100644 --- a/services/api/src/server/handlers/inputs.rs +++ b/services/api/src/server/handlers/inputs.rs @@ -1,4 +1,11 @@ use actix_web::{web, HttpRequest, HttpResponse}; +use fuel_streams_core::types::{ + Address, + AssetId, + BlockHeight, + ContractId, + TxId, +}; use fuel_streams_domains::{ inputs::{queryable::InputsQuery, InputType}, queryable::{Queryable, ValidatedQuery}, @@ -8,6 +15,38 @@ use fuel_web_utils::api_key::ApiKey; use super::{Error, GetDataResponse}; use crate::server::state::ServerState; +#[utoipa::path( + get, + path = "/inputs", + tag = "inputs", + params( + // InputsQuery fields + ("txId" = Option, Query, description = "Filter by transaction ID"), + ("txIndex" = Option, Query, description = "Filter by transaction index"), + ("inputIndex" = Option, Query, description = "Filter by input index"), + ("inputType" = Option, Query, description = "Filter by input type"), + ("blockHeight" = Option, Query, description = "Filter by block height"), + ("ownerId" = Option
, Query, description = "Filter by owner ID (for coin inputs)"), + ("assetId" = Option, Query, description = "Filter by asset ID (for coin inputs)"), + ("contractId" = Option, Query, description = "Filter by contract ID (for contract inputs)"), + ("senderAddress" = Option
, Query, description = "Filter by sender address (for message inputs)"), + ("recipientAddress" = Option
, Query, description = "Filter by recipient address (for message inputs)"), + ("address" = Option
, Query, description = "Filter by address"), + // Flattened QueryPagination fields + ("after" = Option, Query, description = "Return inputs after this height"), + ("before" = Option, Query, description = "Return inputs before this height"), + ("first" = Option, Query, description = "Limit results, sorted by ascending block height", maximum = 100), + ("last" = Option, Query, description = "Limit results, sorted by descending block height", maximum = 100) + ), + responses( + (status = 200, description = "Successfully retrieved inputs", body = GetDataResponse), + (status = 400, description = "Invalid query parameters", body = String), + (status = 500, description = "Internal server error", body = String) + ), + security( + ("api_key" = []) + ) +)] pub async fn get_inputs( req: HttpRequest, req_query: ValidatedQuery, diff --git a/services/api/src/server/handlers/mod.rs b/services/api/src/server/handlers/mod.rs index 178d80c3..bc719992 100644 --- a/services/api/src/server/handlers/mod.rs +++ b/services/api/src/server/handlers/mod.rs @@ -3,6 +3,7 @@ pub mod blocks; pub mod contracts; pub mod inputs; pub mod macros; +pub mod open_api; pub mod outputs; pub mod receipts; pub mod transactions; @@ -19,7 +20,10 @@ use fuel_web_utils::{ api_key::middleware::ApiKeyAuth, server::api::with_prefixed_route, }; +use open_api::ApiDoc; use serde::Serialize; +use utoipa::OpenApi; +use utoipa_swagger_ui::SwaggerUi; use super::handlers; use crate::{ @@ -60,8 +64,7 @@ impl From for actix_web::Error { } } } - -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, utoipa::ToSchema)] #[serde(rename_all = "camelCase")] pub struct GetDataResponse { data: Vec, @@ -212,5 +215,11 @@ pub fn create_services( ("utxos", handlers::accounts::get_accounts_utxos) ] ); + + // Serve the OpenAPI specification as JSON + cfg.service( + SwaggerUi::new("/swagger-ui/{_:.*}") + .url("/api-docs/openapi.json", ApiDoc::openapi()), + ); } } diff --git a/services/api/src/server/handlers/open_api.rs b/services/api/src/server/handlers/open_api.rs new file mode 100644 index 00000000..32297def --- /dev/null +++ b/services/api/src/server/handlers/open_api.rs @@ -0,0 +1,148 @@ +#![allow(clippy::disallowed_methods)] + +use fuel_streams_core::types::{ + Amount, + BlobId, + BlockHeader, + BlockId, + BlockVersion, + BurnReceipt, + CallReceipt, + Consensus, + FuelCoreUpgradePurposeWrapper, + GasAmount, + HexData, + Input, + InputCoin, + InputContract, + InputMessage, + LogDataReceipt, + LogReceipt, + MessageOutReceipt, + MintReceipt, + Nonce, + Output, + OutputChange, + OutputCoin, + OutputContract, + OutputContractCreated, + OutputVariable, + PanicReceipt, + PolicyWrapper, + Receipt, + ReturnDataReceipt, + ReturnReceipt, + RevertReceipt, + Salt, + ScriptResultReceipt, + StorageSlot, + TransferOutReceipt, + TransferReceipt, + TxPointer, + UtxoId, +}; +use fuel_streams_domains::{ + blocks::queryable::BlocksQuery, + inputs::queryable::InputsQuery, + outputs::queryable::OutputsQuery, + receipts::queryable::ReceiptsQuery, + transactions::queryable::TransactionsQuery, +}; +use utoipa::OpenApi; + +use super::{ + accounts::*, + blocks::*, + contracts::*, + inputs::*, + outputs::*, + receipts::*, + transactions::*, + utxos::*, +}; + +#[derive(OpenApi)] +#[openapi( + paths( + get_blocks, + get_block_transactions, + get_block_receipts, + get_block_inputs, + get_block_outputs, + get_accounts_transactions, + get_accounts_inputs, + get_accounts_outputs, + get_accounts_utxos, + get_contracts_transactions, + get_contracts_inputs, + get_contracts_outputs, + get_contracts_utxos, + get_inputs, + get_outputs, + get_receipts, + get_transactions, + get_transaction_receipts, + get_transaction_inputs, + get_transaction_outputs, + get_utxos, + ), + components(schemas( + BlocksQuery, + TransactionsQuery, + ReceiptsQuery, + InputsQuery, + OutputsQuery, + Consensus, + BlockHeader, + BlockId, + BlockVersion, + InputContract, + InputCoin, + InputMessage, + OutputCoin, + OutputContract, + OutputChange, + OutputVariable, + OutputContractCreated, + BlobId, + Input, + Amount, + Output, + PolicyWrapper, + HexData, + Receipt, + Salt, + GasAmount, + StorageSlot, + TxPointer, + FuelCoreUpgradePurposeWrapper, + CallReceipt, + ReturnReceipt, + ReturnDataReceipt, + PanicReceipt, + RevertReceipt, + LogReceipt, + LogDataReceipt, + TransferReceipt, + TransferOutReceipt, + ScriptResultReceipt, + MessageOutReceipt, + MintReceipt, + BurnReceipt, + Nonce, + UtxoId, + )), + tags( + (name = "Blocks", description = "Block retrieval endpoints"), + (name = "Accounts", description = "Accounts retrieval endpoints"), + (name = "Contracts", description = "Contracts retrieval endpoints"), + (name = "Inputs", description = "Inputs retrieval endpoints"), + (name = "Outputs", description = "Outputs retrieval endpoints"), + (name = "Receipts", description = "Receipts retrieval endpoints"), + (name = "Transactions", description = "Transactions retrieval endpoints"), + ), + security( + ("api_key" = []) + ) +)] +pub struct ApiDoc; diff --git a/services/api/src/server/handlers/outputs.rs b/services/api/src/server/handlers/outputs.rs index 4a39bbcc..3f44df63 100644 --- a/services/api/src/server/handlers/outputs.rs +++ b/services/api/src/server/handlers/outputs.rs @@ -1,4 +1,11 @@ use actix_web::{web, HttpRequest, HttpResponse}; +use fuel_streams_core::types::{ + Address, + AssetId, + BlockHeight, + ContractId, + TxId, +}; use fuel_streams_domains::{ outputs::{queryable::OutputsQuery, OutputType}, queryable::{Queryable, ValidatedQuery}, @@ -8,6 +15,36 @@ use fuel_web_utils::api_key::ApiKey; use super::{Error, GetDataResponse}; use crate::server::state::ServerState; +#[utoipa::path( + get, + path = "/outputs", + tag = "outputs", + params( + // OutputsQuery fields + ("txId" = Option, Query, description = "Filter by transaction ID"), + ("txIndex" = Option, Query, description = "Filter by transaction index"), + ("outputIndex" = Option, Query, description = "Filter by output index"), + ("outputType" = Option, Query, description = "Filter by output type"), + ("blockHeight" = Option, Query, description = "Filter by block height"), + ("toAddress" = Option
, Query, description = "Filter by recipient address (for coin, change, and variable outputs)"), + ("assetId" = Option, Query, description = "Filter by asset ID (for coin, change, and variable outputs)"), + ("contractId" = Option, Query, description = "Filter by contract ID (for contract and contract_created outputs)"), + ("address" = Option
, Query, description = "Filter by address"), + // Flattened QueryPagination fields + ("after" = Option, Query, description = "Return outputs after this height"), + ("before" = Option, Query, description = "Return outputs before this height"), + ("first" = Option, Query, description = "Limit results, sorted by ascending block height", maximum = 100), + ("last" = Option, Query, description = "Limit results, sorted by descending block height", maximum = 100) + ), + responses( + (status = 200, description = "Successfully retrieved outputs", body = GetDataResponse), + (status = 400, description = "Invalid query parameters", body = String), + (status = 500, description = "Internal server error", body = String) + ), + security( + ("api_key" = []) + ) +)] pub async fn get_outputs( req: HttpRequest, req_query: ValidatedQuery, diff --git a/services/api/src/server/handlers/receipts.rs b/services/api/src/server/handlers/receipts.rs index 5f3107e4..6ed6ad20 100644 --- a/services/api/src/server/handlers/receipts.rs +++ b/services/api/src/server/handlers/receipts.rs @@ -1,4 +1,12 @@ use actix_web::{web, HttpRequest, HttpResponse}; +use fuel_streams_core::types::{ + Address, + AssetId, + BlockHeight, + Bytes32, + ContractId, + TxId, +}; use fuel_streams_domains::{ queryable::{Queryable, ValidatedQuery}, receipts::{queryable::ReceiptsQuery, ReceiptType}, @@ -8,6 +16,40 @@ use fuel_web_utils::api_key::ApiKey; use super::{Error, GetDataResponse}; use crate::server::state::ServerState; +#[utoipa::path( + get, + path = "/receipts", + tag = "receipts", + params( + // ReceiptsQuery fields + ("txId" = Option, Query, description = "Filter by transaction ID"), + ("txIndex" = Option, Query, description = "Filter by transaction index"), + ("receiptIndex" = Option, Query, description = "Filter by receipt index"), + ("receiptType" = Option, Query, description = "Filter by receipt type"), + ("blockHeight" = Option, Query, description = "Filter by block height"), + ("from" = Option, Query, description = "Filter by source contract ID"), + ("to" = Option, Query, description = "Filter by destination contract ID"), + ("contract" = Option, Query, description = "Filter by contract ID"), + ("asset" = Option, Query, description = "Filter by asset ID"), + ("sender" = Option
, Query, description = "Filter by sender address"), + ("recipient" = Option
, Query, description = "Filter by recipient address"), + ("subId" = Option, Query, description = "Filter by sub ID"), + ("address" = Option
, Query, description = "Filter by address"), + // Flattened QueryPagination fields + ("after" = Option, Query, description = "Return receipts after this height"), + ("before" = Option, Query, description = "Return receipts before this height"), + ("first" = Option, Query, description = "Limit results, sorted by ascending block height", maximum = 100), + ("last" = Option, Query, description = "Limit results, sorted by descending block height", maximum = 100) + ), + responses( + (status = 200, description = "Successfully retrieved receipts", body = GetDataResponse), + (status = 400, description = "Invalid query parameters", body = String), + (status = 500, description = "Internal server error", body = String) + ), + security( + ("api_key" = []) + ) +)] pub async fn get_receipts( req: HttpRequest, req_query: ValidatedQuery, diff --git a/services/api/src/server/handlers/transactions.rs b/services/api/src/server/handlers/transactions.rs index c339bf27..f1e04d70 100644 --- a/services/api/src/server/handlers/transactions.rs +++ b/services/api/src/server/handlers/transactions.rs @@ -1,4 +1,17 @@ use actix_web::{web, HttpRequest, HttpResponse}; +use fuel_streams_core::types::{ + Address, + AssetId, + BlockHeight, + Bytes32, + ContractId, + InputType, + OutputType, + ReceiptType, + TransactionStatus, + TransactionType, + TxId, +}; use fuel_streams_domains::{ inputs::queryable::InputsQuery, outputs::queryable::OutputsQuery, @@ -11,6 +24,34 @@ use fuel_web_utils::api_key::ApiKey; use super::{Error, GetDataResponse}; use crate::server::state::ServerState; +#[utoipa::path( + get, + path = "/transactions", + tag = "transactions", + params( + // TransactionsQuery fields + ("txId" = Option, Query, description = "Filter by transaction ID"), + ("txIndex" = Option, Query, description = "Filter by transaction index"), + ("txStatus" = Option, Query, description = "Filter by transaction status"), + ("type" = Option, Query, description = "Filter by transaction type"), + ("blockHeight" = Option, Query, description = "Filter by block height"), + ("contractId" = Option, Query, description = "Filter by contract ID"), + ("address" = Option
, Query, description = "Filter by address"), + // Flattened QueryPagination fields + ("after" = Option, Query, description = "Return transactions after this height"), + ("before" = Option, Query, description = "Return transactions before this height"), + ("first" = Option, Query, description = "Limit results, sorted by ascending block height", maximum = 100), + ("last" = Option, Query, description = "Limit results, sorted by descending block height", maximum = 100) + ), + responses( + (status = 200, description = "Successfully retrieved transactions", body = GetDataResponse), + (status = 400, description = "Invalid query parameters", body = String), + (status = 500, description = "Internal server error", body = String) + ), + security( + ("api_key" = []) + ) +)] pub async fn get_transactions( req: HttpRequest, req_query: ValidatedQuery, @@ -26,6 +67,42 @@ pub async fn get_transactions( Ok(HttpResponse::Ok().json(response)) } +#[utoipa::path( + get, + path = "/transactions/{txId}/receipts", + tag = "transactions", + params( + // Path parameter + ("txId" = String, Path, description = "Transaction ID"), + // ReceiptsQuery fields (excluding tx_id since it's in the path) + ("txIndex" = Option, Query, description = "Filter by transaction index"), + ("receiptIndex" = Option, Query, description = "Filter by receipt index"), + ("receiptType" = Option, Query, description = "Filter by receipt type"), + ("blockHeight" = Option, Query, description = "Filter by block height"), + ("from" = Option, Query, description = "Filter by source contract ID"), + ("to" = Option, Query, description = "Filter by destination contract ID"), + ("contract" = Option, Query, description = "Filter by contract ID"), + ("asset" = Option, Query, description = "Filter by asset ID"), + ("sender" = Option
, Query, description = "Filter by sender address"), + ("recipient" = Option
, Query, description = "Filter by recipient address"), + ("subId" = Option, Query, description = "Filter by sub ID"), + ("address" = Option
, Query, description = "Filter by address"), + // Flattened QueryPagination fields + ("after" = Option, Query, description = "Return receipts after this height"), + ("before" = Option, Query, description = "Return receipts before this height"), + ("first" = Option, Query, description = "Limit results, sorted by ascending block height", maximum = 100), + ("last" = Option, Query, description = "Limit results, sorted by descending block height", maximum = 100) + ), + responses( + (status = 200, description = "Successfully retrieved transaction receipts", body = GetDataResponse), + (status = 400, description = "Invalid query parameters", body = String), + (status = 404, description = "Transaction not found", body = String), + (status = 500, description = "Internal server error", body = String) + ), + security( + ("api_key" = []) + ) +)] pub async fn get_transaction_receipts( req: HttpRequest, tx_id: web::Path, @@ -44,6 +121,40 @@ pub async fn get_transaction_receipts( Ok(HttpResponse::Ok().json(response)) } +#[utoipa::path( + get, + path = "/transactions/{txId}/inputs", + tag = "transactions", + params( + // Path parameter + ("txId" = String, Path, description = "Transaction ID"), + // InputsQuery fields (excluding tx_id since it's in the path) + ("txIndex" = Option, Query, description = "Filter by transaction index"), + ("inputIndex" = Option, Query, description = "Filter by input index"), + ("inputType" = Option, Query, description = "Filter by input type"), + ("blockHeight" = Option, Query, description = "Filter by block height"), + ("ownerId" = Option
, Query, description = "Filter by owner ID (for coin inputs)"), + ("assetId" = Option, Query, description = "Filter by asset ID (for coin inputs)"), + ("contractId" = Option, Query, description = "Filter by contract ID (for contract inputs)"), + ("senderAddress" = Option
, Query, description = "Filter by sender address (for message inputs)"), + ("recipientAddress" = Option
, Query, description = "Filter by recipient address (for message inputs)"), + ("address" = Option
, Query, description = "Filter by address"), + // Flattened QueryPagination fields + ("after" = Option, Query, description = "Return inputs after this height"), + ("before" = Option, Query, description = "Return inputs before this height"), + ("first" = Option, Query, description = "Limit results, sorted by ascending block height", maximum = 100), + ("last" = Option, Query, description = "Limit results, sorted by descending block height", maximum = 100) + ), + responses( + (status = 200, description = "Successfully retrieved transaction inputs", body = GetDataResponse), + (status = 400, description = "Invalid query parameters", body = String), + (status = 404, description = "Transaction not found", body = String), + (status = 500, description = "Internal server error", body = String) + ), + security( + ("api_key" = []) + ) +)] pub async fn get_transaction_inputs( req: HttpRequest, tx_id: web::Path, @@ -62,6 +173,38 @@ pub async fn get_transaction_inputs( Ok(HttpResponse::Ok().json(response)) } +#[utoipa::path( + get, + path = "/transactions/{txId}/outputs", + tag = "transactions", + params( + // Path parameter + ("txId" = String, Path, description = "Transaction ID"), + // OutputsQuery fields (excluding tx_id since it's in the path) + ("txIndex" = Option, Query, description = "Filter by transaction index"), + ("outputIndex" = Option, Query, description = "Filter by output index"), + ("outputType" = Option, Query, description = "Filter by output type"), + ("blockHeight" = Option, Query, description = "Filter by block height"), + ("toAddress" = Option
, Query, description = "Filter by recipient address (for coin, change, and variable outputs)"), + ("assetId" = Option, Query, description = "Filter by asset ID (for coin, change, and variable outputs)"), + ("contractId" = Option, Query, description = "Filter by contract ID (for contract and contract_created outputs)"), + ("address" = Option
, Query, description = "Filter by address"), + // Flattened QueryPagination fields + ("after" = Option, Query, description = "Return outputs after this height"), + ("before" = Option, Query, description = "Return outputs before this height"), + ("first" = Option, Query, description = "Limit results, sorted by ascending block height", maximum = 100), + ("last" = Option, Query, description = "Limit results, sorted by descending block height", maximum = 100) + ), + responses( + (status = 200, description = "Successfully retrieved transaction outputs", body = GetDataResponse), + (status = 400, description = "Invalid query parameters", body = String), + (status = 404, description = "Transaction not found", body = String), + (status = 500, description = "Internal server error", body = String) + ), + security( + ("api_key" = []) + ) +)] pub async fn get_transaction_outputs( req: HttpRequest, tx_id: web::Path, diff --git a/services/api/src/server/handlers/utxos.rs b/services/api/src/server/handlers/utxos.rs index efa56e6a..979212be 100644 --- a/services/api/src/server/handlers/utxos.rs +++ b/services/api/src/server/handlers/utxos.rs @@ -1,4 +1,12 @@ use actix_web::{web, HttpRequest, HttpResponse}; +use fuel_streams_core::types::{ + Address, + BlockHeight, + ContractId, + HexData, + InputType, + TxId, +}; use fuel_streams_domains::{ queryable::{Queryable, ValidatedQuery}, utxos::queryable::UtxosQuery, @@ -8,6 +16,35 @@ use fuel_web_utils::api_key::ApiKey; use super::{Error, GetDataResponse}; use crate::server::state::ServerState; +#[utoipa::path( + get, + path = "/utxos", + tag = "utxos", + params( + // UtxosQuery fields + ("txId" = Option, Query, description = "Filter by transaction ID"), + ("txIndex" = Option, Query, description = "Filter by transaction index"), + ("inputIndex" = Option, Query, description = "Filter by input index"), + ("utxoType" = Option, Query, description = "Filter by UTXO type"), + ("blockHeight" = Option, Query, description = "Filter by block height"), + ("utxoId" = Option, Query, description = "Filter by UTXO ID"), + ("contractId" = Option, Query, description = "Filter by contract ID"), + ("address" = Option
, Query, description = "Filter by address"), + // Flattened QueryPagination fields + ("after" = Option, Query, description = "Return UTXOs after this height"), + ("before" = Option, Query, description = "Return UTXOs before this height"), + ("first" = Option, Query, description = "Limit results, sorted by ascending block height", maximum = 100), + ("last" = Option, Query, description = "Limit results, sorted by descending block height", maximum = 100) + ), + responses( + (status = 200, description = "Successfully retrieved UTXOs", body = GetDataResponse), + (status = 400, description = "Invalid query parameters", body = String), + (status = 500, description = "Internal server error", body = String) + ), + security( + ("api_key" = []) + ) +)] pub async fn get_utxos( req: HttpRequest, req_query: ValidatedQuery,