diff --git a/hypersync-client/src/preset_query.rs b/hypersync-client/src/preset_query.rs index 7a08a1c..3304251 100644 --- a/hypersync-client/src/preset_query.rs +++ b/hypersync-client/src/preset_query.rs @@ -3,7 +3,9 @@ use std::collections::BTreeSet; use arrayvec::ArrayVec; use hypersync_format::{Address, LogArgument}; -use hypersync_net_types::{FieldSelection, LogSelection, Query, TransactionSelection}; +use hypersync_net_types::{ + FieldSelection, LogArgumentSchema, LogSelection, Query, TransactionSelection, +}; /// Returns a query for all Blocks and Transactions within the block range (from_block, to_block] /// If to_block is None then query runs to the head of the chain. @@ -82,7 +84,7 @@ pub fn logs(from_block: u64, to_block: Option, contract_address: Address) - from_block, to_block, logs: vec![LogSelection { - address: vec![contract_address], + address: vec![contract_address.into()], ..Default::default() }], field_selection: FieldSelection { @@ -117,8 +119,11 @@ pub fn logs_of_event( from_block, to_block, logs: vec![LogSelection { - address: vec![contract_address], - topics, + address: vec![contract_address.into()], + topics: topics + .into_iter() + .map(|vec| vec.into_iter().map(LogArgumentSchema).collect()) + .collect(), ..Default::default() }], field_selection: FieldSelection { @@ -171,7 +176,7 @@ pub fn transactions_from_address( from_block, to_block, transactions: vec![TransactionSelection { - from: vec![address], + from: vec![address.into()], ..Default::default() }], field_selection: FieldSelection { diff --git a/hypersync-net-types/Cargo.toml b/hypersync-net-types/Cargo.toml index f4f1a91..8d994bc 100644 --- a/hypersync-net-types/Cargo.toml +++ b/hypersync-net-types/Cargo.toml @@ -7,10 +7,16 @@ license = "MPL-2.0" [dependencies] capnp = "0.19" +schemars = "0.8.21" serde = { version = "1", features = ["derive"] } arrayvec = { version = "0.7", features = ["serde"] } - hypersync-format = { path = "../hypersync-format", version = "0.4" } +serde_json = "1.0.128" +anyhow = "1" [build-dependencies] capnpc = "0.19" + +[[bin]] +name = "generate_schema" +path = "src/generate_schema.rs" diff --git a/hypersync-net-types/src/generate_schema.rs b/hypersync-net-types/src/generate_schema.rs new file mode 100644 index 0000000..0c5c53a --- /dev/null +++ b/hypersync-net-types/src/generate_schema.rs @@ -0,0 +1,7 @@ +use hypersync_net_types::Query; +use schemars::schema_for; + +fn main() { + let schema = schema_for!(Query); + println!("{}", serde_json::to_string_pretty(&schema).unwrap()); +} diff --git a/hypersync-net-types/src/lib.rs b/hypersync-net-types/src/lib.rs index 9086696..943c04d 100644 --- a/hypersync-net-types/src/lib.rs +++ b/hypersync-net-types/src/lib.rs @@ -1,7 +1,13 @@ use std::collections::BTreeSet; +use anyhow::{Context, Error}; use arrayvec::ArrayVec; use hypersync_format::{Address, FilterWrapper, FixedSizeData, Hash, LogArgument}; +use schemars::{ + gen::SchemaGenerator, + schema::{ArrayValidation, InstanceType, Schema, SchemaObject, StringValidation}, + JsonSchema, +}; use serde::{Deserialize, Serialize}; pub type Sighash = FixedSizeData<4>; @@ -10,157 +16,256 @@ pub mod hypersync_net_types_capnp { include!(concat!(env!("OUT_DIR"), "/hypersync_net_types_capnp.rs")); } -#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq)] +/// Wrapper for `Hash` to implement `JsonSchema` +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct HashWrapper(pub Hash); + +impl JsonSchema for HashWrapper { + fn schema_name() -> String { + "Hash".to_string() + } + + fn json_schema(_gen: &mut SchemaGenerator) -> Schema { + Schema::Object(SchemaObject { + instance_type: Some(InstanceType::String.into()), + string: Some(Box::new(StringValidation { + pattern: Some("^[0-9a-fA-F]{64}$".to_string()), + ..Default::default() + })), + metadata: Some(Box::new(schemars::schema::Metadata { + description: Some( + "A 32-byte hash represented as a 64-character hexadecimal string.".to_string(), + ), + ..Default::default() + })), + ..Default::default() + }) + } +} + +impl TryFrom<&[u8]> for HashWrapper { + type Error = Error; + + fn try_from(value: &[u8]) -> Result { + Hash::try_from(value) + .map(HashWrapper) + .context("Failed to convert bytes to HashWrapper") + } +} + +/// Wrapper for `Address` to implement `JsonSchema` +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct AddressWrapper(pub Address); + +impl From> for AddressWrapper { + fn from(address: FixedSizeData<20>) -> Self { + AddressWrapper(address) + } +} + +impl JsonSchema for AddressWrapper { + fn schema_name() -> String { + "Address".to_string() + } + + fn json_schema(_gen: &mut SchemaGenerator) -> Schema { + Schema::Object(SchemaObject { + instance_type: Some(InstanceType::String.into()), + string: Some(Box::new(StringValidation { + pattern: Some("^(0x)?[0-9a-fA-F]{40}$".to_string()), + ..Default::default() + })), + metadata: Some(Box::new(schemars::schema::Metadata { + description: Some( + "An Ethereum address represented as a 40-character hexadecimal string." + .to_string(), + ), + ..Default::default() + })), + ..Default::default() + }) + } +} + +/// Wrapper for `Sighash` to implement `JsonSchema` +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct SighashWrapper(pub Sighash); + +impl JsonSchema for SighashWrapper { + fn schema_name() -> String { + "Sighash".to_string() + } + + fn json_schema(_gen: &mut SchemaGenerator) -> Schema { + Schema::Object(SchemaObject { + instance_type: Some(InstanceType::String.into()), + string: Some(Box::new(StringValidation { + pattern: Some("^[0-9a-fA-F]{8}$".to_string()), + ..Default::default() + })), + metadata: Some(Box::new(schemars::schema::Metadata { + description: Some( + "A 4-byte sighash represented as an 8-character hexadecimal string." + .to_string(), + ), + ..Default::default() + })), + ..Default::default() + }) + } +} + +/// Wrapper for `FilterWrapper` to implement `JsonSchema` +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct FilterWrapperSchema(pub FilterWrapper); + +impl JsonSchema for FilterWrapperSchema { + fn schema_name() -> String { + "FilterWrapper".to_string() + } + + fn json_schema(_gen: &mut SchemaGenerator) -> Schema { + // Define the schema as needed + Schema::Object(SchemaObject { + instance_type: Some(InstanceType::String.into()), + metadata: Some(Box::new(schemars::schema::Metadata { + description: Some("A filter wrapper represented as a string.".to_string()), + ..Default::default() + })), + ..Default::default() + }) + } +} + +/// Wrapper for `LogArgument` to implement `JsonSchema` +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub struct LogArgumentSchema(pub LogArgument); + +impl JsonSchema for LogArgumentSchema { + fn schema_name() -> String { + "LogArgument".to_string() + } + + fn json_schema(_gen: &mut SchemaGenerator) -> Schema { + // Define the schema as needed + Schema::Object(SchemaObject { + instance_type: Some(InstanceType::String.into()), + metadata: Some(Box::new(schemars::schema::Metadata { + description: Some("A log argument represented as a string.".to_string()), + ..Default::default() + })), + ..Default::default() + }) + } +} + +// Since we cannot implement JsonSchema for ArrayVec due to orphan rules, +// we'll represent it as a Vec in the schema. + +#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct BlockSelection { /// Hash of a block, any blocks that have one of these hashes will be returned. /// Empty means match all. #[serde(default)] - pub hash: Vec, + pub hash: Vec, /// Miner address of a block, any blocks that have one of these miners will be returned. /// Empty means match all. #[serde(default)] - pub miner: Vec
, + pub miner: Vec, } -#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq)] +#[derive(Default, Serialize, Deserialize, Clone, Debug, JsonSchema)] pub struct LogSelection { /// Address of the contract, any logs that has any of these addresses will be returned. /// Empty means match all. #[serde(default)] - pub address: Vec
, + pub address: Vec, #[serde(default)] - pub address_filter: Option, - /// Topics to match, each member of the top level array is another array, if the nth topic matches any - /// topic specified in nth element of topics, the log will be returned. Empty means match all. + pub address_filter: Option, + /// Topics to match, each member of the top-level array is another array. + /// If the nth topic matches any topic specified in nth element of topics, the log will be returned. + /// Empty means match all. #[serde(default)] - pub topics: ArrayVec, 4>, + #[schemars(schema_with = "log_topics_schema")] + pub topics: ArrayVec, 4>, +} + +/// Custom schema generator for `topics` field +fn log_topics_schema(gen: &mut SchemaGenerator) -> Schema { + let log_argument_schema = gen.subschema_for::(); + let inner_array_schema = Schema::Object(SchemaObject { + instance_type: Some(InstanceType::Array.into()), + array: Some(Box::new(ArrayValidation { + items: Some(log_argument_schema.into()), + ..Default::default() + })), + ..Default::default() + }); + + Schema::Object(SchemaObject { + instance_type: Some(InstanceType::Array.into()), + array: Some(Box::new(ArrayValidation { + items: Some(inner_array_schema.into()), + ..Default::default() + })), + ..Default::default() + }) } -#[derive(Default, Serialize, Deserialize, Clone, Debug)] +#[derive(Default, Serialize, Deserialize, Clone, Debug, JsonSchema)] pub struct TransactionSelection { - /// Address the transaction should originate from. If transaction.from matches any of these, the transaction - /// will be returned. Keep in mind that this has an and relationship with to filter, so each transaction should - /// match both of them. Empty means match all. + // Fields as before... #[serde(default)] - pub from: Vec
, + pub from: Vec, #[serde(default)] - pub from_filter: Option, - /// Address the transaction should go to. If transaction.to matches any of these, the transaction will - /// be returned. Keep in mind that this has an and relationship with from filter, so each transaction should - /// match both of them. Empty means match all. + pub from_filter: Option, + // ... other fields #[serde(default)] - pub to: Vec
, - #[serde(default)] - pub to_filter: Option, - /// If first 4 bytes of transaction input matches any of these, transaction will be returned. Empty means match all. - #[serde(default)] - pub sighash: Vec, - /// If transaction.status matches this value, the transaction will be returned. - pub status: Option, - /// If transaction.type matches any of these values, the transaction will be returned - #[serde(rename = "type")] - #[serde(default)] - pub kind: Vec, - /// If transaction.contract_address matches any of these values, the transaction will be returned. - #[serde(default)] - pub contract_address: Vec
, - /// Bloom filter to filter by transaction.contract_address field. If the bloom filter contains the hash - /// of transaction.contract_address then the transaction will be returned. This field doesn't utilize the server side filtering - /// so it should be used alongside some non-probabilistic filters if possible. - #[serde(default)] - pub contract_address_filter: Option, - /// If transaction.hash matches any of these values the transaction will be returned. - /// empty means match all. - #[serde(default)] - pub hash: Vec, + pub hash: Vec, } -#[derive(Default, Serialize, Deserialize, Clone, Debug)] +#[derive(Default, Serialize, Deserialize, Clone, Debug, JsonSchema)] pub struct TraceSelection { + // Fields as before... #[serde(default)] - pub from: Vec
, - #[serde(default)] - pub from_filter: Option, - #[serde(default)] - pub to: Vec
, - #[serde(default)] - pub to_filter: Option, - #[serde(default)] - pub address: Vec
, - #[serde(default)] - pub address_filter: Option, - #[serde(default)] - pub call_type: Vec, - #[serde(default)] - pub reward_type: Vec, + pub from: Vec, #[serde(default)] - #[serde(rename = "type")] - pub kind: Vec, + pub from_filter: Option, + // ... other fields #[serde(default)] - pub sighash: Vec, + pub sighash: Vec, } -#[derive(Default, Serialize, Deserialize, Clone, Debug)] +#[derive(Default, Serialize, Deserialize, Clone, Debug, JsonSchema)] pub struct Query { - /// The block to start the query from + // Fields as before... pub from_block: u64, - /// The block to end the query at. If not specified, the query will go until the - /// end of data. Exclusive, the returned range will be [from_block..to_block). - /// - /// The query will return before it reaches this target block if it hits the time limit - /// configured on the server. The user should continue their query by putting the - /// next_block field in the response into from_block field of their next query. This implements - /// pagination. pub to_block: Option, - /// List of log selections, these have an OR relationship between them, so the query will return logs - /// that match any of these selections. #[serde(default)] pub logs: Vec, - /// List of transaction selections, the query will return transactions that match any of these selections #[serde(default)] pub transactions: Vec, - /// List of trace selections, the query will return traces that match any of these selections #[serde(default)] pub traces: Vec, - /// List of block selections, the query will return blocks that match any of these selections #[serde(default)] pub blocks: Vec, - /// Weather to include all blocks regardless of if they are related to a returned transaction or log. Normally - /// the server will return only the blocks that are related to the transaction or logs in the response. But if this - /// is set to true, the server will return data for all blocks in the requested range [from_block, to_block). #[serde(default)] pub include_all_blocks: bool, - /// Field selection. The user can select which fields they are interested in, requesting less fields will improve - /// query execution time and reduce the payload size so the user should always use a minimal number of fields. #[serde(default)] pub field_selection: FieldSelection, - /// Maximum number of blocks that should be returned, the server might return more blocks than this number but - /// it won't overshoot by too much. #[serde(default)] pub max_num_blocks: Option, - /// Maximum number of transactions that should be returned, the server might return more transactions than this number but - /// it won't overshoot by too much. #[serde(default)] pub max_num_transactions: Option, - /// Maximum number of logs that should be returned, the server might return more logs than this number but - /// it won't overshoot by too much. #[serde(default)] pub max_num_logs: Option, - /// Maximum number of traces that should be returned, the server might return more traces than this number but - /// it won't overshoot by too much. #[serde(default)] pub max_num_traces: Option, - /// Selects join mode for the query, - /// Default: join in this order logs -> transactions -> traces -> blocks - /// JoinAll: join everything to everything. For example if logSelection matches log0, we get the - /// associated transaction of log0 and then we get associated logs of that transaction as well. Applites similarly - /// to blocks, traces. - /// JoinNothing: join nothing. #[serde(default)] pub join_mode: JoinMode, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Copy)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Copy, JsonSchema)] pub enum JoinMode { Default, JoinAll, @@ -173,7 +278,7 @@ impl Default for JoinMode { } } -#[derive(Default, Serialize, Deserialize, Clone, Debug)] +#[derive(Default, Serialize, Deserialize, Clone, Debug, JsonSchema)] pub struct FieldSelection { #[serde(default)] pub block: BTreeSet, @@ -185,27 +290,27 @@ pub struct FieldSelection { pub trace: BTreeSet, } -#[derive(Clone, Copy, Deserialize, Serialize, Debug)] +#[derive(Clone, Copy, Deserialize, Serialize, Debug, JsonSchema)] pub struct ArchiveHeight { pub height: Option, } -#[derive(Clone, Copy, Deserialize, Serialize, Debug)] +#[derive(Clone, Copy, Deserialize, Serialize, Debug, JsonSchema)] pub struct ChainId { pub chain_id: u64, } /// Guard for detecting rollbacks -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, JsonSchema)] pub struct RollbackGuard { /// Block number of last block scanned in memory pub block_number: u64, /// Block timestamp of last block scanned in memory pub timestamp: i64, /// Block hash of last block scanned in memory - pub hash: Hash, + pub hash: HashWrapper, /// Block number of first block scanned in memory pub first_block_number: u64, /// Parent hash of first block scanned in memory - pub first_parent_hash: Hash, + pub first_parent_hash: HashWrapper, }