Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(katana): sequencer feeder gateway client #2732

Merged
merged 10 commits into from
Nov 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ members = [
"crates/katana/controller",
"crates/katana/core",
"crates/katana/executor",
"crates/katana/feeder-gateway",
"crates/katana/grpc",
"crates/katana/node",
"crates/katana/node-bindings",
Expand Down Expand Up @@ -97,6 +98,7 @@ katana-codecs-derive = { path = "crates/katana/storage/codecs/derive" }
katana-core = { path = "crates/katana/core", default-features = false }
katana-db = { path = "crates/katana/storage/db" }
katana-executor = { path = "crates/katana/executor" }
katana-feeder-gateway = { path = "crates/katana/feeder-gateway" }
katana-node = { path = "crates/katana/node", default-features = false }
katana-node-bindings = { path = "crates/katana/node-bindings" }
katana-pipeline = { path = "crates/katana/pipeline" }
Expand Down
20 changes: 20 additions & 0 deletions crates/katana/feeder-gateway/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
edition.workspace = true
license.workspace = true
license-file.workspace = true
name = "katana-feeder-gateway"
repository.workspace = true
version.workspace = true

[dependencies]
katana-primitives.workspace = true
katana-rpc-types.workspace = true

reqwest.workspace = true
serde.workspace = true
starknet.workspace = true
thiserror.workspace = true
url.workspace = true

[dev-dependencies]
tokio.workspace = true
252 changes: 252 additions & 0 deletions crates/katana/feeder-gateway/src/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
use katana_primitives::block::{BlockIdOrTag, BlockTag};
use katana_primitives::class::CasmContractClass;
use katana_primitives::Felt;
use reqwest::{Client, StatusCode};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use url::Url;

use crate::types::{ContractClass, StateUpdate, StateUpdateWithBlock};

#[derive(Debug, thiserror::Error)]

Check warning on line 11 in crates/katana/feeder-gateway/src/client.rs

View check run for this annotation

Codecov / codecov/patch

crates/katana/feeder-gateway/src/client.rs#L11

Added line #L11 was not covered by tests
pub enum Error {
#[error(transparent)]
Network(#[from] reqwest::Error),

#[error(transparent)]
Sequencer(SequencerError),

#[error("Request rate limited")]
RateLimited,
}

/// Client for interacting with the Starknet's feeder gateway.
#[derive(Debug, Clone)]
pub struct SequencerGateway {
base_url: Url,
client: Client,
}

impl SequencerGateway {
/// Creates a new gateway client to Starknet mainnet.
///
/// https://docs.starknet.io/tools/important-addresses/#sequencer_base_url
pub fn sn_mainnet() -> Self {
Self::new(Url::parse("https://alpha-mainnet.starknet.io/").unwrap())
}

Check warning on line 36 in crates/katana/feeder-gateway/src/client.rs

View check run for this annotation

Codecov / codecov/patch

crates/katana/feeder-gateway/src/client.rs#L34-L36

Added lines #L34 - L36 were not covered by tests

/// Creates a new gateway client to Starknet sepolia.
///
/// https://docs.starknet.io/tools/important-addresses/#sequencer_base_url
pub fn sn_sepolia() -> Self {
Self::new(Url::parse("https://alpha-sepolia.starknet.io/").unwrap())
}

Check warning on line 43 in crates/katana/feeder-gateway/src/client.rs

View check run for this annotation

Codecov / codecov/patch

crates/katana/feeder-gateway/src/client.rs#L41-L43

Added lines #L41 - L43 were not covered by tests

/// Creates a new gateway client at the given base URL.
pub fn new(base_url: Url) -> Self {
let client = Client::new();
Self { client, base_url }
}

Check warning on line 49 in crates/katana/feeder-gateway/src/client.rs

View check run for this annotation

Codecov / codecov/patch

crates/katana/feeder-gateway/src/client.rs#L46-L49

Added lines #L46 - L49 were not covered by tests

pub async fn get_state_update(&self, block_id: BlockIdOrTag) -> Result<StateUpdate, Error> {
self.feeder_gateway("get_state_update").with_block_id(block_id).send().await
}

Check warning on line 53 in crates/katana/feeder-gateway/src/client.rs

View check run for this annotation

Codecov / codecov/patch

crates/katana/feeder-gateway/src/client.rs#L51-L53

Added lines #L51 - L53 were not covered by tests

pub async fn get_state_update_with_block(
&self,
block_id: BlockIdOrTag,
) -> Result<StateUpdateWithBlock, Error> {
self.feeder_gateway("get_state_update")
.with_query_param("includeBlock", "true")
.with_block_id(block_id)
.send()
.await
}

Check warning on line 64 in crates/katana/feeder-gateway/src/client.rs

View check run for this annotation

Codecov / codecov/patch

crates/katana/feeder-gateway/src/client.rs#L55-L64

Added lines #L55 - L64 were not covered by tests

pub async fn get_class(
&self,
hash: Felt,
block_id: BlockIdOrTag,
) -> Result<ContractClass, Error> {
self.feeder_gateway("get_class_by_hash")
.with_query_param("classHash", &format!("{hash:#x}"))
.with_block_id(block_id)
.send()
.await
}

Check warning on line 76 in crates/katana/feeder-gateway/src/client.rs

View check run for this annotation

Codecov / codecov/patch

crates/katana/feeder-gateway/src/client.rs#L66-L76

Added lines #L66 - L76 were not covered by tests

pub async fn get_compiled_class(
&self,
hash: Felt,
block_id: BlockIdOrTag,
) -> Result<CasmContractClass, Error> {
self.feeder_gateway("get_compiled_class_by_class_hash")
.with_query_param("classHash", &format!("{hash:#x}"))
.with_block_id(block_id)
.send()
.await
}

Check warning on line 88 in crates/katana/feeder-gateway/src/client.rs

View check run for this annotation

Codecov / codecov/patch

crates/katana/feeder-gateway/src/client.rs#L78-L88

Added lines #L78 - L88 were not covered by tests

fn feeder_gateway(&self, method: &str) -> RequestBuilder<'_> {
let mut url = self.base_url.clone();
url.path_segments_mut().expect("invalid base url").extend(["feeder_gateway", method]);
RequestBuilder { client: &self.client, url }
}

Check warning on line 94 in crates/katana/feeder-gateway/src/client.rs

View check run for this annotation

Codecov / codecov/patch

crates/katana/feeder-gateway/src/client.rs#L90-L94

Added lines #L90 - L94 were not covered by tests
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum Response<T> {
Data(T),
Error(SequencerError),
}

#[derive(Debug, Clone)]
struct RequestBuilder<'a> {
client: &'a Client,
url: Url,
}

impl<'a> RequestBuilder<'a> {
fn with_block_id(self, block_id: BlockIdOrTag) -> Self {
match block_id {
// latest block is implied, if no block id specified
BlockIdOrTag::Tag(BlockTag::Latest) => self,
BlockIdOrTag::Tag(BlockTag::Pending) => self.with_query_param("blockNumber", "pending"),
BlockIdOrTag::Hash(hash) => self.with_query_param("blockHash", &format!("{hash:#x}")),
BlockIdOrTag::Number(num) => self.with_query_param("blockNumber", &num.to_string()),
}
}

fn with_query_param(mut self, key: &str, value: &str) -> Self {
self.url.query_pairs_mut().append_pair(key, value);
self
}

async fn send<T: DeserializeOwned>(self) -> Result<T, Error> {
let response = self.client.get(self.url).send().await?;
if response.status() == StatusCode::TOO_MANY_REQUESTS {
Err(Error::RateLimited)

Check warning on line 129 in crates/katana/feeder-gateway/src/client.rs

View check run for this annotation

Codecov / codecov/patch

crates/katana/feeder-gateway/src/client.rs#L126-L129

Added lines #L126 - L129 were not covered by tests
} else {
match response.json::<Response<T>>().await? {
Response::Data(data) => Ok(data),
Response::Error(error) => Err(Error::Sequencer(error)),

Check warning on line 133 in crates/katana/feeder-gateway/src/client.rs

View check run for this annotation

Codecov / codecov/patch

crates/katana/feeder-gateway/src/client.rs#L131-L133

Added lines #L131 - L133 were not covered by tests
}
}
}

Check warning on line 136 in crates/katana/feeder-gateway/src/client.rs

View check run for this annotation

Codecov / codecov/patch

crates/katana/feeder-gateway/src/client.rs#L136

Added line #L136 was not covered by tests
}

#[derive(Debug, thiserror::Error, Deserialize)]

Check warning on line 139 in crates/katana/feeder-gateway/src/client.rs

View check run for this annotation

Codecov / codecov/patch

crates/katana/feeder-gateway/src/client.rs#L139

Added line #L139 was not covered by tests
#[error("{message} ({code:?})")]
pub struct SequencerError {
pub code: ErrorCode,
pub message: String,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]

Check warning on line 146 in crates/katana/feeder-gateway/src/client.rs

View check run for this annotation

Codecov / codecov/patch

crates/katana/feeder-gateway/src/client.rs#L146

Added line #L146 was not covered by tests
pub enum ErrorCode {
#[serde(rename = "StarknetErrorCode.BLOCK_NOT_FOUND")]
BlockNotFound,
#[serde(rename = "StarknetErrorCode.ENTRY_POINT_NOT_FOUND_IN_CONTRACT")]
EntryPointNotFoundInContract,
#[serde(rename = "StarknetErrorCode.INVALID_PROGRAM")]
InvalidProgram,
#[serde(rename = "StarknetErrorCode.TRANSACTION_FAILED")]
TransactionFailed,
#[serde(rename = "StarknetErrorCode.TRANSACTION_NOT_FOUND")]
TransactionNotFound,
#[serde(rename = "StarknetErrorCode.UNINITIALIZED_CONTRACT")]
UninitializedContract,
#[serde(rename = "StarkErrorCode.MALFORMED_REQUEST")]
MalformedRequest,
#[serde(rename = "StarknetErrorCode.UNDECLARED_CLASS")]
UndeclaredClass,
#[serde(rename = "StarknetErrorCode.INVALID_TRANSACTION_NONCE")]
InvalidTransactionNonce,
#[serde(rename = "StarknetErrorCode.VALIDATE_FAILURE")]
ValidateFailure,
#[serde(rename = "StarknetErrorCode.CLASS_ALREADY_DECLARED")]
ClassAlreadyDeclared,
#[serde(rename = "StarknetErrorCode.COMPILATION_FAILED")]
CompilationFailed,
#[serde(rename = "StarknetErrorCode.INVALID_COMPILED_CLASS_HASH")]
InvalidCompiledClassHash,
#[serde(rename = "StarknetErrorCode.DUPLICATED_TRANSACTION")]
DuplicatedTransaction,
#[serde(rename = "StarknetErrorCode.INVALID_CONTRACT_CLASS")]
InvalidContractClass,
#[serde(rename = "StarknetErrorCode.DEPRECATED_ENDPOINT")]
DeprecatedEndpoint,
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn request_block_id() {
let client = Client::new();
let base_url = Url::parse("https://example.com/").unwrap();
let req = RequestBuilder { client: &client, url: base_url };

// Test pending block
let pending_url = req.clone().with_block_id(BlockIdOrTag::Tag(BlockTag::Pending)).url;
assert_eq!(pending_url.query(), Some("blockNumber=pending"));

// Test block hash
let hash = Felt::from(123);
let hash_url = req.clone().with_block_id(BlockIdOrTag::Hash(hash)).url;
assert_eq!(hash_url.query(), Some("blockHash=0x7b"));

// Test block number
let num_url = req.clone().with_block_id(BlockIdOrTag::Number(42)).url;
assert_eq!(num_url.query(), Some("blockNumber=42"));

// Test latest block (should have no query params)
let latest_url = req.with_block_id(BlockIdOrTag::Tag(BlockTag::Latest)).url;
assert_eq!(latest_url.query(), None);
}

#[test]
fn multiple_query_params() {
let client = Client::new();
let base_url = Url::parse("https://example.com/").unwrap();
let req = RequestBuilder { client: &client, url: base_url };

let url = req
.with_query_param("param1", "value1")
.with_query_param("param2", "value2")
.with_query_param("param3", "value3")
.url;

let query = url.query().unwrap();
assert!(query.contains("param1=value1"));
assert!(query.contains("param2=value2"));
assert!(query.contains("param3=value3"));
}

#[test]
#[ignore]
fn request_block_id_overwrite() {
let client = Client::new();
let base_url = Url::parse("https://example.com/").unwrap();
let req = RequestBuilder { client: &client, url: base_url };

let url = req
.clone()
.with_block_id(BlockIdOrTag::Tag(BlockTag::Pending))
.with_block_id(BlockIdOrTag::Number(42))
.url;

assert_eq!(url.query(), Some("blockNumber=42"));

Check warning on line 241 in crates/katana/feeder-gateway/src/client.rs

View check run for this annotation

Codecov / codecov/patch

crates/katana/feeder-gateway/src/client.rs#L230-L241

Added lines #L230 - L241 were not covered by tests

let hash = Felt::from(123);
let url = req
.clone()
.with_block_id(BlockIdOrTag::Hash(hash))
.with_block_id(BlockIdOrTag::Tag(BlockTag::Pending))
.url;

assert_eq!(url.query(), Some("blockNumber=pending"));
}

Check warning on line 251 in crates/katana/feeder-gateway/src/client.rs

View check run for this annotation

Codecov / codecov/patch

crates/katana/feeder-gateway/src/client.rs#L243-L251

Added lines #L243 - L251 were not covered by tests
}
4 changes: 4 additions & 0 deletions crates/katana/feeder-gateway/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#![cfg_attr(not(test), warn(unused_crate_dependencies))]

pub mod client;
pub mod types;
Loading
Loading