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

fix: contract raw_value deserialization #1

Open
wants to merge 10 commits into
base: fork
Choose a base branch
from
16 changes: 7 additions & 9 deletions starknet-core/src/types/contract/legacy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ const API_VERSION: Felt = Felt::ZERO;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
pub struct LegacyContractClass {
pub abi: Vec<RawLegacyAbiEntry>,
#[serde(skip_serializing_if = "Option::is_none")]
pub abi: Option<Vec<RawLegacyAbiEntry>>,
pub entry_points_by_type: RawLegacyEntryPoints,
pub program: LegacyProgram,
}
Expand Down Expand Up @@ -459,7 +460,7 @@ impl LegacyContractClass {
}

let serialized = to_string_pythonic(&ContractArtifactForHash {
abi: &self.abi,
abi: &self.abi.clone().unwrap_or_default(),
program: &self.program,
})
.map_err(|err| {
Expand All @@ -476,13 +477,10 @@ impl LegacyContractClass {
Ok(CompressedLegacyContractClass {
program: self.program.compress()?,
entry_points_by_type: self.entry_points_by_type.clone().into(),
abi: Some(
self.abi
.clone()
.into_iter()
.map(|item| item.into())
.collect(),
),
abi: match &self.abi {
Some(abi) => Some(abi.clone().into_iter().map(|item| item.into()).collect()),
None => None,
},
})
}
}
Expand Down
37 changes: 14 additions & 23 deletions starknet-core/src/types/receipt_block.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use serde::{Deserialize, Serialize};
use serde_with::serde_as;

use crate::serde::unsigned_field_element::UfeHex;
use starknet_types_core::felt::Felt;

/// A more idiomatic way to access `execution_status` and `revert_reason`.
Expand Down Expand Up @@ -48,21 +49,22 @@ impl ReceiptBlock {
}
}

#[serde_as]
#[derive(Serialize, Deserialize)]
#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
struct Raw {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde_as(as = "Option<UfeHex>")]
block_hash: Option<Felt>,
#[serde(skip_serializing_if = "Option::is_none")]
block_number: Option<u64>,
}

impl Serialize for ReceiptBlock {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
#[derive(Serialize)]
#[serde_as]
struct Raw<'a> {
#[serde_as(as = "Option<UfeHex>")]
#[serde(skip_serializing_if = "Option::is_none")]
block_hash: Option<&'a Felt>,
#[serde(skip_serializing_if = "Option::is_none")]
block_number: Option<&'a u64>,
}

let raw = match self {
Self::Pending => Raw {
block_hash: None,
Expand All @@ -72,8 +74,8 @@ impl Serialize for ReceiptBlock {
block_hash,
block_number,
} => Raw {
block_hash: Some(block_hash),
block_number: Some(block_number),
block_hash: Some(*block_hash),
block_number: Some(*block_number),
},
};

Expand All @@ -86,17 +88,6 @@ impl<'de> Deserialize<'de> for ReceiptBlock {
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde_as]
#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
struct Raw {
#[serde_as(as = "Option<UfeHex>")]
#[serde(skip_serializing_if = "Option::is_none")]
block_hash: Option<Felt>,
#[serde(skip_serializing_if = "Option::is_none")]
block_number: Option<u64>,
}

let raw = Raw::deserialize(deserializer)?;

match (raw.block_hash, raw.block_number) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"code": "StarknetErrorCode.BLOCK_NOT_FOUND", "message": "Block hash 0x0 does not exist."}
2 changes: 1 addition & 1 deletion starknet-providers/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ url = "2.3.1"
reqwest = { version = "0.11.16", default-features = false, features = ["rustls-tls"] }
thiserror = "1.0.40"
serde = "1.0.160"
serde_json = "1.0.96"
serde_json = { version = "1.0.96" }
serde_with = "2.3.2"

[target.'cfg(target_arch = "wasm32")'.dependencies]
Expand Down
94 changes: 72 additions & 22 deletions starknet-providers/src/sequencer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,7 @@ impl SequencerGatewayProvider {
url
}

async fn send_get_request<T>(&self, url: Url) -> Result<T, ProviderError>
where
T: DeserializeOwned,
{
async fn send_get_request_raw(&self, url: Url) -> Result<String, ProviderError> {
trace!("Sending GET request to sequencer API ({})", url);

let mut request = self.client.get(url);
Expand All @@ -206,13 +203,21 @@ impl SequencerGatewayProvider {
Err(ProviderError::RateLimited)
} else {
let body = res.text().await.map_err(GatewayClientError::Network)?;

trace!("Response from sequencer API: {}", body);

Ok(serde_json::from_str(&body).map_err(GatewayClientError::Serde)?)
Ok(body)
}
}

async fn send_get_request<T>(&self, url: Url) -> Result<T, ProviderError>
where
T: DeserializeOwned,
{
let body = self.send_get_request_raw(url).await?;

Ok(serde_json::from_str(&body).map_err(GatewayClientError::Serde)?)
}

async fn send_post_request<Q, S>(&self, url: Url, body: &Q) -> Result<S, ProviderError>
where
Q: Serialize,
Expand Down Expand Up @@ -316,6 +321,24 @@ impl SequencerGatewayProvider {
.into()
}

#[deprecated(
note = "Sequencer-specific functions are deprecated. Use it via the Provider trait instead."
)]
pub async fn get_state_update_with_block(
&self,
block_identifier: BlockId,
) -> Result<StateUpdateWithBlock, ProviderError> {
let mut request_url = self.extend_feeder_gateway_url("get_state_update");
append_block_id(&mut request_url, block_identifier);
request_url
.query_pairs_mut()
.append_pair("includeBlock", "true");

self.send_get_request::<GatewayResponse<_>>(request_url)
.await?
.into()
}

#[deprecated(
note = "Sequencer-specific functions are deprecated. Use it via the Provider trait instead."
)]
Expand Down Expand Up @@ -349,9 +372,23 @@ impl SequencerGatewayProvider {
.append_pair("classHash", &format!("{class_hash:#x}"));
append_block_id(&mut request_url, block_identifier);

self.send_get_request::<GatewayResponse<_>>(request_url)
.await?
.into()
// `body` can be a `FlattenedSierraClass`, a `LegacyContractClass` or a `SequenceError`.
// All are "untagged", meaning we have to try them out sequencialy to find out which it is.
// Due to `serde` limitations, we cannot express this inside a `Deserialize` impl while using `raw_value`,
// so we don't follow the regular deserialization flow, and do it outside of the trait.
let body = self.send_get_request_raw(request_url).await?.into_bytes();

if let Ok(value) = DeployedClass::deserialize_from_json_deployed_class(&body) {
return Ok(value);
}
if let Ok(value) = serde_json::from_slice::<SequencerError>(&body) {
return Err(value.into());
}

Err(GatewayClientError::Serde(serde::de::Error::custom(
"data did not match any variant of enum GatewayResponse",
))
.into())
}

#[deprecated(
Expand Down Expand Up @@ -571,33 +608,46 @@ mod tests {
]
.into_iter()
{
serde_json::from_str::<GatewayResponse<DeployedClass>>(raw).unwrap();
DeployedClass::deserialize_from_json_deployed_class(raw.as_bytes()).unwrap();
}
}

#[test]
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
fn test_get_class_by_hash_deser_not_declared() {
match serde_json::from_str::<GatewayResponse<DeployedClass>>(include_str!(
"../../test-data/raw_gateway_responses/get_class_by_hash/2_not_declared.txt"
fn test_get_block_deser_does_not_exist() {
match serde_json::from_str::<GatewayResponse<Block>>(include_str!(
"../../test-data/raw_gateway_responses/get_block/16_does_not_exist.txt"
))
.unwrap()
{
GatewayResponse::SequencerError(err) => {
assert_eq!(err.code, ErrorCode::UndeclaredClass);
assert_eq!(err.code, ErrorCode::BlockNotFound);
}
_ => panic!("Unexpected result"),
}
}

#[test]
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
fn test_error_deser_invalid_contract_class() {
let error: SequencerError = serde_json::from_str(include_str!(
"../../test-data/serde/sequencer_error_invalid_contract_class.json"
))
.unwrap();
#[tokio::test]
#[allow(deprecated)]
async fn big_felt_are_deserialized_using_raw_data() {
let provider = SequencerGatewayProvider::starknet_alpha_mainnet();

let class_hash = Felt::from_hex_unchecked(
"0x50ca5f07e12e74a7f0c4dea9cdd26b34a29780be00d1430a6d511f933ec9ec6",
);

let contract_class = provider
.get_class_by_hash(class_hash, BlockId::Latest)
.await
.unwrap();

let computed_class_hash = match contract_class {
DeployedClass::SierraClass(_) => panic!("this should be a legacy contract"),
DeployedClass::LegacyClass(cc) => cc.class_hash().unwrap(),
};

assert_eq!(error.code, ErrorCode::InvalidContractClass);
// This contract contains big fields, if it is deserialized without using `raw_values`,
// those will be treated as float and the values will change, therefore chainging the class hash
assert_eq!(class_hash, computed_class_hash);
}
}
73 changes: 55 additions & 18 deletions starknet-providers/src/sequencer/models/contract.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::io::Write;
use std::{fmt::Formatter, io::Write};

use flate2::{write::GzEncoder, Compression};
use serde::{Deserialize, Deserializer, Serialize};
use serde::{de::Error, Deserialize, Deserializer, Serialize};
use serde_with::serde_as;
use starknet_core::{
serde::{byte_array::base64::serialize as base64_ser, unsigned_field_element::UfeHex},
Expand Down Expand Up @@ -52,23 +52,60 @@ pub enum DecompressProgramError {
Io(std::io::Error),
}

// We need to manually implement this because `raw_value` doesn't work with `untagged`:
// https://github.com/serde-rs/serde/issues/1183
impl<'de> Deserialize<'de> for DeployedClass {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let temp_value = serde_json::Value::deserialize(deserializer)?;
if let Ok(value) = FlattenedSierraClass::deserialize(&temp_value) {
return Ok(Self::SierraClass(value));
impl DeployedClass {
// We need to manually implement this because `raw_value` doesn't work with `untagged`:
// https://github.com/serde-rs/serde/issues/1183
// Preventing us to impl `Deserialize` on DeployedClass in a satisfying way.
pub fn deserialize_from_json_deployed_class(json: &[u8]) -> Result<Self, serde_json::Error> {
// Those are the fields of both `FlattenedSierraClass` and `LegacyClass` mixed together.
// We don't deserialize their content, we just check whether or not those fields are present.
#[derive(Deserialize)]
struct BulkDeployedClassFields<'a> {
// Common fields
#[serde(borrow)]
pub entry_points_by_type: &'a serde_json::value::RawValue,
#[serde(borrow)]
pub abi: &'a serde_json::value::RawValue,
// Sierra contract specific fields
#[serde(borrow)]
#[serde(skip_serializing_if = "Option::is_none")]
pub sierra_program: Option<&'a serde_json::value::RawValue>,
#[serde(borrow)]
#[serde(skip_serializing_if = "Option::is_none")]
pub contract_class_version: Option<&'a serde_json::value::RawValue>,
// Cairo countract specific field
#[serde(borrow)]
#[serde(skip_serializing_if = "Option::is_none")]
pub program: Option<&'a serde_json::value::RawValue>,
}
if let Ok(value) = LegacyContractClass::deserialize(&temp_value) {
return Ok(Self::LegacyClass(value));
}
Err(serde::de::Error::custom(
"data did not match any variant of enum DeployedClass",
))

let buld_fields: BulkDeployedClassFields = serde_json::from_slice(json).unwrap();

let deployed_class = match buld_fields.program {
Some(program) => DeployedClass::LegacyClass(LegacyContractClass {
abi: serde_json::from_str(buld_fields.abi.get())?,
entry_points_by_type: serde_json::from_str(buld_fields.entry_points_by_type.get())?,
program: serde_json::from_str(program.get())?,
}),
None => DeployedClass::SierraClass(FlattenedSierraClass {
sierra_program: serde_json::from_str(
buld_fields
.sierra_program
.ok_or(serde_json::Error::missing_field("sierra_program"))?
.get(),
)?,
contract_class_version: serde_json::from_str(
buld_fields
.contract_class_version
.ok_or(serde_json::Error::missing_field("contract_class_version"))?
.get(),
)?,
entry_points_by_type: serde_json::from_str(buld_fields.entry_points_by_type.get())?,
abi: serde_json::from_str(buld_fields.abi.get())?,
}),
};

Ok(deployed_class)
}
}

Expand Down
7 changes: 4 additions & 3 deletions starknet-providers/src/sequencer/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ pub use block::{Block, BlockId, BlockStatus};

mod transaction;
pub use transaction::{
DeclareTransaction, DeployAccountTransaction, DeployTransaction, EntryPointType,
InvokeFunctionTransaction, L1HandlerTransaction, TransactionFailureReason, TransactionInfo,
TransactionStatusInfo, TransactionType,
DataAvailabilityMode, DeclareTransaction, DeployAccountTransaction, DeployTransaction,
EntryPointType, InvokeFunctionTransaction, L1HandlerTransaction, ResourceBoundsMapping,
TransactionFailureReason, TransactionInfo, TransactionStatusInfo, TransactionType,
};

mod transaction_receipt;
Expand Down Expand Up @@ -47,6 +47,7 @@ pub use contract::{CompressedLegacyContractClass, DeployedClass};

pub mod state_update;
pub use state_update::StateUpdate;
pub use state_update::StateUpdateWithBlock;

pub mod trace;
pub use trace::{BlockTraces, TransactionTrace};
Expand Down
10 changes: 10 additions & 0 deletions starknet-providers/src/sequencer/models/state_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use serde_with::serde_as;
use starknet_core::{serde::unsigned_field_element::UfeHex, types::Felt};
use std::collections::HashMap;

use super::Block;

#[serde_as]
#[derive(Debug, Deserialize)]
#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
Expand All @@ -16,6 +18,14 @@ pub struct StateUpdate {
pub state_diff: StateDiff,
}

#[serde_as]
#[derive(Debug, Deserialize)]
#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
pub struct StateUpdateWithBlock {
pub state_update: StateUpdate,
pub block: Block,
}

#[serde_as]
#[derive(Debug, Deserialize)]
#[cfg_attr(feature = "no_unknown_fields", serde(deny_unknown_fields))]
Expand Down
Loading