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

Implementing a code generator for starknet-types-rpc #9

Merged
merged 28 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
30ece8b
✨ initial implementation of `starknet-types-rpc`
nils-mathieu Oct 29, 2023
1b61c5b
implement `Copy` for simple enum types
nils-mathieu Oct 29, 2023
96c3edc
update documentation
nils-mathieu Oct 29, 2023
656249f
add some basic tests
nils-mathieu Oct 29, 2023
d22651d
re-export the \'stark-felt\' type
nils-mathieu Oct 29, 2023
5e28a48
remove method result types
nils-mathieu Oct 29, 2023
6d0b56d
remove debug symbol paths from the generated file for better clarity
nils-mathieu Oct 30, 2023
0bc5573
add support for #[no_std]
nils-mathieu Oct 30, 2023
525e026
fix: properly derive Serialize/Deserialize on param types
nils-mathieu Oct 30, 2023
276a431
remove openrpc-gen binary file
nils-mathieu Oct 30, 2023
2b1bc16
update the README to include how to build the library from source
nils-mathieu Oct 30, 2023
810e3aa
configure the generator to avoid using `std`
nils-mathieu Oct 30, 2023
48b3d20
remove trash files
nils-mathieu Oct 30, 2023
0fc7240
avoid using stark-felt/std when the std feature is not enabled
nils-mathieu Oct 30, 2023
e6576a2
merge from 'main'
nils-mathieu Nov 1, 2023
ca99315
convert dependency `stark-felt` to `starknet-types-core`
nils-mathieu Nov 1, 2023
81c8742
fix the tag of BroadcastedDeclareTxn
nils-mathieu Nov 1, 2023
861bf2b
move custom types in a 'custom' module
nils-mathieu Nov 1, 2023
2b6043e
take query-only broadcasted transactions
nils-mathieu Nov 1, 2023
229b6bc
update the spec to the lastest version
nils-mathieu Nov 1, 2023
42cbd46
move everything in a v0_5_0 module for better backwards compatibility
nils-mathieu Nov 1, 2023
5b7231c
include other files of the specification
nils-mathieu Nov 1, 2023
7aa5f3e
remove paths from the trace_api generated file
nils-mathieu Nov 1, 2023
30e0df9
renamed copied file
nils-mathieu Nov 1, 2023
7275cc3
update to the latest version of the spec
nils-mathieu Nov 6, 2023
1a0980e
optimize the hex-convertion routines + add comments
nils-mathieu Nov 7, 2023
6846446
add tests for NumAsHex serialization/deserialization routines
nils-mathieu Nov 7, 2023
997b609
remove usage of `String` within tests
nils-mathieu Nov 7, 2023
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
10 changes: 10 additions & 0 deletions crates/starknet-types-rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,14 @@ keywords = ["stark", "zkp", "cairo"]
description = "Starknet RPC types."
readme = "README.md"

[features]
default = ["std"]

std = ["serde/std", "starknet-types-core/std"]

[dependencies]
starknet-types-core = { path = "../starknet-types-core", default-features = false, features = ["serde"] }
serde = { version = "1", default-features = false, features = ["derive"] }

[dev-dependencies]
serde_json = "1"
21 changes: 20 additions & 1 deletion crates/starknet-types-rpc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,26 @@ starknet-types-rpc = { version = "0.0.2", git = "https://github.com/starknet-io/

## Build from source

Clone the repository and navigate to the starknet-types-rpc directory. Then run:
The crate is built in two steps:

### Generating bindings against the Starknet OpenRPC specification

The specification is hosted on Starknet's repository ([link](https://github.com/starkware-libs/starknet-specs/blob/master/api/starknet_api_openrpc.json)).

Bindings are generated using [`openrpc-gen`](https://github.com/nils-mathieu/openrpc-gen).

After having built `openrpc-gen`, you can use the following command to generate the final generated
Rust files:

```bash
openrpc-gen --config configs/v0.5.0.toml --document configs/spec_v0.5.0.json --output src/generated/v0.5.0.rs
```

*Note that this first step is normally already done for you upon cloning the repository.*

### Building the generated files

Once this is done, you can build the crate with:

```bash
cargo build --release
Expand Down
67 changes: 67 additions & 0 deletions crates/starknet-types-rpc/src/custom/block_id.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use serde::{Deserialize, Deserializer, Serialize};

use crate::{BlockHash, BlockNumber, BlockTag};

/// A hexadecimal number.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BlockId {
/// The tag of the block.
Tag(BlockTag),
/// The hash of the block.
Hash(BlockHash),
/// The height of the block.
Number(BlockNumber),
}

#[derive(Serialize, Deserialize)]
struct BlockHashHelper {
block_hash: BlockHash,
}

#[derive(Serialize, Deserialize)]
struct BlockNumberHelper {
block_number: BlockNumber,
}

#[derive(Deserialize)]
#[serde(untagged)]
enum BlockIdHelper {
Tag(BlockTag),
Hash(BlockHashHelper),
Number(BlockNumberHelper),
}

impl serde::Serialize for BlockId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
BlockId::Tag(tag) => tag.serialize(serializer),
BlockId::Hash(hash) => {
let helper = BlockHashHelper { block_hash: *hash };
helper.serialize(serializer)
}
BlockId::Number(number) => {
let helper = BlockNumberHelper {
block_number: *number,
};
helper.serialize(serializer)
}
}
}
}

impl<'de> serde::Deserialize<'de> for BlockId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let helper = BlockIdHelper::deserialize(deserializer)?;
match helper {
BlockIdHelper::Tag(tag) => Ok(BlockId::Tag(tag)),
BlockIdHelper::Hash(helper) => Ok(BlockId::Hash(helper.block_hash)),
BlockIdHelper::Number(helper) => Ok(BlockId::Number(helper.block_number)),
}
}
}
7 changes: 7 additions & 0 deletions crates/starknet-types-rpc/src/custom/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
mod block_id;
mod query;
mod syncing_status;

pub use self::block_id::*;
pub use self::query::*;
pub use self::syncing_status::*;
48 changes: 48 additions & 0 deletions crates/starknet-types-rpc/src/custom/query.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// query offset:
// 0x0000000000000000000000000000000100000000000000000000000000000000

use serde::{Deserialize, Serialize};

use crate::{
BroadcastedDeclareTxnV1, BroadcastedDeclareTxnV2, DeployAccountTxnV1, InvokeTxnV0, InvokeTxnV1,
};

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "version")]
pub enum BroadcastedDeclareTxn {
#[serde(rename = "0x1")]
V1(BroadcastedDeclareTxnV1),
#[serde(rename = "0x2")]
V2(BroadcastedDeclareTxnV2),
/// Query-only broadcasted declare transaction.
#[serde(rename = "0x0000000000000000000000000000000100000000000000000000000000000001")]
QueryV1(BroadcastedDeclareTxnV1),
/// Query-only broadcasted declare transaction.
#[serde(rename = "0x0000000000000000000000000000000100000000000000000000000000000002")]
QueryV2(BroadcastedDeclareTxnV2),
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "version")]
pub enum BroadcastedDeployAccountTxn {
#[serde(rename = "0x1")]
V1(DeployAccountTxnV1),
/// Query-only broadcasted deploy account transaction.
#[serde(rename = "0x0000000000000000000000000000000100000000000000000000000000000001")]
QueryV1(DeployAccountTxnV1),
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "version")]
pub enum BroadcastedInvokeTxn {
#[serde(rename = "0x0")]
V0(InvokeTxnV0),
#[serde(rename = "0x1")]
V1(InvokeTxnV1),
/// Query-only broadcasted invoke transaction.
#[serde(rename = "0x0000000000000000000000000000000100000000000000000000000000000000")]
QueryV0(InvokeTxnV0),
/// Query-only broadcasted invoke transaction.
#[serde(rename = "0x0000000000000000000000000000000100000000000000000000000000000001")]
QueryV1(InvokeTxnV1),
}
65 changes: 65 additions & 0 deletions crates/starknet-types-rpc/src/custom/syncing_status.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use serde::de::Visitor;
use serde::{Deserialize, Deserializer, Serialize, Serializer};

use crate::SyncStatus;

/// The syncing status of a node.
#[derive(Clone, Debug)]
pub enum SyncingStatus {
/// The node is not syncing.
NotSyncing,
/// The node is syncing.
Syncing(SyncStatus),
}

impl Serialize for SyncingStatus {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
SyncingStatus::NotSyncing => serializer.serialize_bool(false),
SyncingStatus::Syncing(status) => status.serialize(serializer),
}
}
}

impl<'de> Deserialize<'de> for SyncingStatus {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct SyncingStatusVisitor;

impl<'de> Visitor<'de> for SyncingStatusVisitor {
type Value = SyncingStatus;

fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
writeln!(formatter, "a syncing status")
}

fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if v {
Err(serde::de::Error::custom("expected a syncing status"))
} else {
Ok(SyncingStatus::NotSyncing)
}
}

fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let status =
SyncStatus::deserialize(serde::de::value::MapAccessDeserializer::new(map))?;

Ok(SyncingStatus::Syncing(status))
}
}

deserializer.deserialize_any(SyncingStatusVisitor)
}
}
137 changes: 137 additions & 0 deletions crates/starknet-types-rpc/src/custom_serde.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
use core::marker::PhantomData;

/// A trait for types that should be serialized or deserialized as hexadecimal strings.
pub trait NumAsHex<'de>: Sized {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer;

fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>;
}

impl<'de> NumAsHex<'de> for u64 {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
const HEX_DIGITS: &[u8] = b"0123456789abcdef";
const MAX_NUMBER_SIZE: usize = u64::MAX.ilog(16) as usize + 1;

// This can be very much optimized using unsafe code.
// Let's benchmark it first to ensure that it's actually worth it.

let mut buffer = [0u8; MAX_NUMBER_SIZE + 2]; // + 2 to account for 0x
let mut cursor = buffer.iter_mut().rev();
let mut n = *self;
while n != 0 {
*cursor.next().unwrap() = HEX_DIGITS[(n % 16) as usize];
n /= 16;
}
*cursor.next().unwrap() = b'x';
*cursor.next().unwrap() = b'0';

let remaining = cursor.len();

// SAFETY:
// We only wrote ASCII characters to the buffer, ensuring that it is only composed
// of valid UTF-8 code points.
let s = core::str::from_utf8(&buffer[remaining..]).unwrap();
0xLucqs marked this conversation as resolved.
Show resolved Hide resolved

serializer.serialize_str(s)
}

fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct NumAsHexVisitor;

impl<'de> serde::de::Visitor<'de> for NumAsHexVisitor {
type Value = u64;

fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
formatter.write_str("a hexadecimal string")
}

fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let Some(digits) = v.strip_prefix("0x") else {
return Err(E::custom("expected a hexadecimal string starting with 0x"));
};

let mut n = 0u64;
for b in digits.bytes() {
let unit = match b {
b'0'..=b'9' => b as u64 - b'0' as u64,
b'a'..=b'f' => b as u64 - b'a' as u64 + 10,
b'A'..=b'F' => b as u64 - b'A' as u64 + 10,
0xLucqs marked this conversation as resolved.
Show resolved Hide resolved
_ => return Err(E::custom("invalid hexadecimal digit")),
};

let Some(next_n) = n.checked_mul(16).and_then(|n| n.checked_add(unit)) else {
return Err(E::custom("integer overflowed 64-bit"));
};

n = next_n;
}

Ok(n)
}
}

deserializer.deserialize_str(NumAsHexVisitor)
}
}

impl<'de, T> NumAsHex<'de> for Option<T>
where
T: NumAsHex<'de>,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
None => serializer.serialize_none(),
Some(v) => v.serialize(serializer),
}
}

fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct OptionVisitor<T>(PhantomData<T>);

impl<'de, T> serde::de::Visitor<'de> for OptionVisitor<T>
where
T: NumAsHex<'de>,
{
type Value = Option<T>;

fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
writeln!(formatter, "an optional number as a hexadecimal string")
}

fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}

fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: serde::Deserializer<'de>,
{
T::deserialize(deserializer).map(Some)
}
}

deserializer.deserialize_option(OptionVisitor(PhantomData))
}
}
Loading