Skip to content

Commit 504fe4e

Browse files
committed
feat: add methods and client support for 0.14.1
1 parent eff2d26 commit 504fe4e

39 files changed

+4890
-1
lines changed

madara/crates/client/db/src/view/block_preconfirmed.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ impl<D: MadaraStorageRead> MadaraPreconfirmedBlockView<D> {
364364
nonces,
365365
deployed_contracts,
366366
replaced_classes,
367+
migrated_compiled_classes: vec![], // TODO(prakhar,22/11/2025): Update this
367368
})
368369
}
369370

@@ -461,6 +462,7 @@ mod tests {
461462
}],
462463
old_declared_contracts: vec![Felt::from(400u64)],
463464
replaced_classes: vec![],
465+
migrated_compiled_classes: vec![] // TODO(prakhar,22/11/2025): Add value here and update tests
464466
},
465467
transactions: vec![],
466468
events: vec![],
@@ -601,6 +603,7 @@ mod tests {
601603
NonceUpdate { contract_address: Felt::from(101u64), nonce: Felt::from(1u64) }, // 0->1
602604
NonceUpdate { contract_address: Felt::from(102u64), nonce: Felt::from(2u64) }, // 0->2
603605
],
606+
migrated_compiled_classes: vec![] // TODO(prakhar,22/11/2025): Add value here and update tests
604607
}
605608
);
606609
}

madara/crates/client/devnet/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ impl ChainGenesisDescription {
235235
deployed_contracts: self.deployed_contracts.as_state_diff(),
236236
replaced_classes: vec![],
237237
nonces: vec![],
238+
migrated_compiled_classes: vec![], // TODO(prakhar,22/11/2025): Update this
238239
},
239240
transactions: vec![],
240241
events: vec![],

madara/crates/client/rpc/src/block_id.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,71 @@ impl BlockViewResolvable for mp_rpc::v0_9_0::BlockId {
139139
}
140140
}
141141

142+
// v0.10 rpc
143+
144+
impl StateViewResolvable for mp_rpc::v0_10_0::BlockId {
145+
fn resolve_state_view(&self, starknet: &Starknet) -> Result<MadaraStateView, StarknetRpcApiError> {
146+
match self {
147+
Self::Tag(mp_rpc::v0_10_0::BlockTag::PreConfirmed) => Ok(starknet.backend.view_on_latest()),
148+
Self::Tag(mp_rpc::v0_10_0::BlockTag::Latest) => Ok(starknet.backend.view_on_latest_confirmed()),
149+
Self::Tag(mp_rpc::v0_10_0::BlockTag::L1Accepted) => starknet
150+
.backend
151+
.latest_l1_confirmed_block_n()
152+
.and_then(|block_number| starknet.backend.view_on_confirmed(block_number))
153+
.ok_or(StarknetRpcApiError::NoBlocks),
154+
Self::Hash(hash) => {
155+
if let Some(block_n) = starknet.backend.view_on_latest().find_block_by_hash(hash)? {
156+
Ok(starknet.backend.view_on_confirmed(block_n).with_context(|| {
157+
format!("Block with hash {hash:#x} was found at {block_n} but no such block exists")
158+
})?)
159+
} else {
160+
Err(StarknetRpcApiError::BlockNotFound)
161+
}
162+
}
163+
Self::Number(block_n) => {
164+
starknet.backend.view_on_confirmed(*block_n).ok_or(StarknetRpcApiError::BlockNotFound)
165+
}
166+
}
167+
}
168+
}
169+
170+
impl BlockViewResolvable for mp_rpc::v0_10_0::BlockId {
171+
fn resolve_block_view(&self, starknet: &Starknet) -> Result<MadaraBlockView, StarknetRpcApiError> {
172+
match self {
173+
Self::Tag(mp_rpc::v0_10_0::BlockTag::PreConfirmed) => {
174+
Ok(starknet.backend.block_view_on_preconfirmed_or_fake()?.into())
175+
}
176+
Self::Tag(mp_rpc::v0_10_0::BlockTag::Latest) => {
177+
starknet.backend.block_view_on_last_confirmed().map(|b| b.into()).ok_or(StarknetRpcApiError::NoBlocks)
178+
}
179+
Self::Tag(mp_rpc::v0_10_0::BlockTag::L1Accepted) => starknet
180+
.backend
181+
.latest_l1_confirmed_block_n()
182+
.and_then(|block_number| starknet.backend.block_view_on_confirmed(block_number))
183+
.map(|b| b.into())
184+
.ok_or(StarknetRpcApiError::NoBlocks),
185+
Self::Hash(hash) => {
186+
if let Some(block_n) = starknet.backend.db.find_block_hash(hash)? {
187+
Ok(starknet
188+
.backend
189+
.block_view_on_confirmed(block_n)
190+
.with_context(|| {
191+
format!("Block with hash {hash:#x} was found at {block_n} but no such block exists")
192+
})?
193+
.into())
194+
} else {
195+
Err(StarknetRpcApiError::BlockNotFound)
196+
}
197+
}
198+
Self::Number(block_n) => starknet
199+
.backend
200+
.block_view_on_confirmed(*block_n)
201+
.map(Into::into)
202+
.ok_or(StarknetRpcApiError::BlockNotFound),
203+
}
204+
}
205+
}
206+
142207
impl Starknet {
143208
pub fn resolve_block_view<R: BlockViewResolvable>(
144209
&self,

madara/crates/client/rpc/src/lib.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//! interface for interacting with the Starknet node. This module implements the official Starknet
33
//! JSON-RPC specification along with some Madara-specific extensions.
44
//!
5-
//! Madara fully supports the Starknet JSON-RPC specification versions `v0.7.1` and `v0.8.1`, with
5+
//! Madara fully supports the Starknet JSON-RPC specification versions `v0.7.1`, `v0.8.1`, `v0.9.0`, and `v0.10.0`, with
66
//! methods accessible through port **9944** by default (configurable via `--rpc-port`). The RPC
77
//! server supports both HTTP and WebSocket connections on the same port.
88
//!
@@ -13,6 +13,8 @@
1313
//!
1414
//! - Default (v0.7.1): `http://localhost:9944/`
1515
//! - Version 0.8.1: `http://localhost:9944/rpc/v0_8_1/`
16+
//! - Version 0.9.0: `http://localhost:9944/rpc/v0_9_0/`
17+
//! - Version 0.10.0: `http://localhost:9944/rpc/v0_10_0/`
1618
//!
1719
//! ## Available Endpoints
1820
//!
@@ -881,6 +883,11 @@ pub fn rpc_api_user(starknet: &Starknet) -> anyhow::Result<RpcModule<()>> {
881883
rpc_api.merge(versions::user::v0_9_0::StarknetWsRpcApiV0_9_0Server::into_rpc(starknet.clone()))?;
882884
rpc_api.merge(versions::user::v0_9_0::StarknetTraceRpcApiV0_9_0Server::into_rpc(starknet.clone()))?;
883885

886+
rpc_api.merge(versions::user::v0_10_0::StarknetReadRpcApiV0_10_0Server::into_rpc(starknet.clone()))?;
887+
rpc_api.merge(versions::user::v0_10_0::StarknetWriteRpcApiV0_10_0Server::into_rpc(starknet.clone()))?;
888+
rpc_api.merge(versions::user::v0_10_0::StarknetWsRpcApiV0_10_0Server::into_rpc(starknet.clone()))?;
889+
rpc_api.merge(versions::user::v0_10_0::StarknetTraceRpcApiV0_10_0Server::into_rpc(starknet.clone()))?;
890+
884891
Ok(rpc_api)
885892
}
886893

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod v0_7_1;
22
pub mod v0_8_1;
33
pub mod v0_9_0;
4+
pub mod v0_10_0;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
pub mod read;
2+
pub mod trace;
3+
pub mod write;
4+
pub mod ws;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
use crate::errors::StarknetRpcApiError;
2+
use crate::errors::StarknetRpcResult;
3+
use crate::Starknet;
4+
use mc_exec::MadaraBlockViewExecutionExt;
5+
use mc_exec::EXECUTION_UNSUPPORTED_BELOW_VERSION;
6+
use mp_rpc::v0_10_0::{BlockId, FunctionCall};
7+
use starknet_types_core::felt::Felt;
8+
9+
/// Call a Function in a Contract Without Creating a Transaction
10+
///
11+
/// ### Arguments
12+
///
13+
/// * `request` - The details of the function call to be made. This includes information such as the
14+
/// contract address, function signature, and arguments.
15+
/// * `block_id` - The identifier of the block used to reference the state or call the transaction
16+
/// on. This can be the hash of the block, its number (height), or a specific block tag.
17+
///
18+
/// ### Returns
19+
///
20+
/// * `result` - The function's return value, as defined in the Cairo output. This is an array of
21+
/// field elements (`Felt`).
22+
///
23+
/// ### Errors
24+
///
25+
/// This method may return the following errors:
26+
/// * `CONTRACT_NOT_FOUND` - If the specified contract address does not exist.
27+
/// * `CONTRACT_ERROR` - If there is an error with the contract or the function call.
28+
/// * `BLOCK_NOT_FOUND` - If the specified block does not exist in the blockchain.
29+
pub async fn call(starknet: &Starknet, request: FunctionCall, block_id: BlockId) -> StarknetRpcResult<Vec<Felt>> {
30+
let view = starknet.resolve_block_view(block_id)?;
31+
32+
let mut exec_context = view.new_execution_context()?;
33+
34+
if exec_context.protocol_version < EXECUTION_UNSUPPORTED_BELOW_VERSION {
35+
return Err(StarknetRpcApiError::unsupported_txn_version());
36+
}
37+
38+
let FunctionCall { contract_address, entry_point_selector, calldata } = request;
39+
// spawn_blocking: avoid starving the tokio workers during execution.
40+
let results = mp_utils::spawn_blocking(move || {
41+
exec_context.call_contract(&contract_address, &entry_point_selector, &calldata)
42+
})
43+
.await?;
44+
45+
Ok(results)
46+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
use crate::errors::StarknetRpcApiError;
2+
use crate::errors::StarknetRpcResult;
3+
use crate::utils::tx_api_to_blockifier;
4+
use crate::Starknet;
5+
use blockifier::transaction::account_transaction::ExecutionFlags;
6+
use mc_exec::execution::TxInfo;
7+
use mc_exec::MadaraBlockViewExecutionExt;
8+
use mc_exec::EXECUTION_UNSUPPORTED_BELOW_VERSION;
9+
use mp_convert::ToFelt;
10+
use mp_rpc::v0_10_0::{BlockId, BroadcastedTxn, FeeEstimate, SimulationFlagForEstimateFee};
11+
use mp_transactions::{IntoStarknetApiExt, ToBlockifierError};
12+
use starknet_types_core::felt::Felt;
13+
14+
/// Extract contract address from a broadcasted transaction for validation
15+
fn get_contract_address_from_tx(tx: &BroadcastedTxn) -> Option<Felt> {
16+
match tx {
17+
BroadcastedTxn::Invoke(invoke_tx) => {
18+
match invoke_tx {
19+
mp_rpc::v0_10_0::BroadcastedInvokeTxn::V0(tx) => Some(tx.contract_address),
20+
mp_rpc::v0_10_0::BroadcastedInvokeTxn::V1(tx) => Some(tx.sender_address),
21+
mp_rpc::v0_10_0::BroadcastedInvokeTxn::V3(tx) => Some(tx.sender_address),
22+
mp_rpc::v0_10_0::BroadcastedInvokeTxn::QueryV0(tx) => Some(tx.contract_address),
23+
mp_rpc::v0_10_0::BroadcastedInvokeTxn::QueryV1(tx) => Some(tx.sender_address),
24+
mp_rpc::v0_10_0::BroadcastedInvokeTxn::QueryV3(tx) => Some(tx.sender_address),
25+
}
26+
}
27+
BroadcastedTxn::Declare(declare_tx) => {
28+
match declare_tx {
29+
mp_rpc::v0_10_0::BroadcastedDeclareTxn::V1(tx) => Some(tx.sender_address),
30+
mp_rpc::v0_10_0::BroadcastedDeclareTxn::V2(tx) => Some(tx.sender_address),
31+
mp_rpc::v0_10_0::BroadcastedDeclareTxn::V3(tx) => Some(tx.sender_address),
32+
mp_rpc::v0_10_0::BroadcastedDeclareTxn::QueryV1(tx) => Some(tx.sender_address),
33+
mp_rpc::v0_10_0::BroadcastedDeclareTxn::QueryV2(tx) => Some(tx.sender_address),
34+
mp_rpc::v0_10_0::BroadcastedDeclareTxn::QueryV3(tx) => Some(tx.sender_address),
35+
}
36+
}
37+
// DeployAccount transactions are deploying new contracts, so we don't check for existence
38+
BroadcastedTxn::DeployAccount(_) => None,
39+
}
40+
}
41+
42+
/// Estimate the fee associated with transaction
43+
///
44+
/// # Arguments
45+
///
46+
/// * `request` - starknet transaction request
47+
/// * `block_id` - hash of the requested block, number (height), or tag
48+
///
49+
/// # Returns
50+
///
51+
/// * `fee_estimate` - fee estimate in gwei
52+
pub async fn estimate_fee(
53+
starknet: &Starknet,
54+
request: Vec<BroadcastedTxn>,
55+
simulation_flags: Vec<SimulationFlagForEstimateFee>,
56+
block_id: BlockId,
57+
) -> StarknetRpcResult<Vec<FeeEstimate>> {
58+
tracing::debug!("estimate fee on block_id {block_id:?}");
59+
let view = starknet.resolve_block_view(block_id)?;
60+
let mut exec_context = view.new_execution_context()?;
61+
62+
if exec_context.protocol_version < EXECUTION_UNSUPPORTED_BELOW_VERSION {
63+
return Err(StarknetRpcApiError::unsupported_txn_version());
64+
}
65+
66+
let validate = !simulation_flags.contains(&SimulationFlagForEstimateFee::SkipValidate);
67+
68+
// RPC 0.10.0: Check contract existence for transactions
69+
let state_view = view.state_view();
70+
for tx in &request {
71+
if let Some(contract_address) = get_contract_address_from_tx(tx) {
72+
if state_view.get_contract_class_hash(&contract_address)?.is_none() {
73+
return Err(StarknetRpcApiError::ContractNotFound {
74+
error: format!("Contract at address {:#x} not found", contract_address).into(),
75+
});
76+
}
77+
}
78+
}
79+
80+
let transactions = request
81+
.into_iter()
82+
.map(|tx| {
83+
let only_query = tx.is_query();
84+
let (api_tx, _) =
85+
tx.into_starknet_api(view.backend().chain_config().chain_id.to_felt(), exec_context.protocol_version)?;
86+
let execution_flags = ExecutionFlags { only_query, charge_fee: false, validate, strict_nonce_check: true };
87+
Ok(tx_api_to_blockifier(api_tx, execution_flags)?)
88+
})
89+
.collect::<Result<Vec<_>, ToBlockifierError>>()?;
90+
91+
let tips = transactions.iter().map(|tx| tx.tip().unwrap_or_default()).collect::<Vec<_>>();
92+
93+
// spawn_blocking: avoid starving the tokio workers during execution.
94+
let (execution_results, exec_context) = mp_utils::spawn_blocking(move || {
95+
Ok::<_, mc_exec::Error>((exec_context.execute_transactions([], transactions)?, exec_context))
96+
})
97+
.await?;
98+
99+
let fee_estimates = execution_results
100+
.iter()
101+
.zip(tips)
102+
.enumerate()
103+
.map(|(index, (result, tip))| {
104+
if result.execution_info.is_reverted() {
105+
return Err(StarknetRpcApiError::TxnExecutionError {
106+
tx_index: index,
107+
error: result.execution_info.revert_error.as_ref().map(|e| e.to_string()).unwrap_or_default(),
108+
});
109+
}
110+
Ok(FeeEstimate {
111+
common: exec_context.execution_result_to_fee_estimate_v0_9(result, tip)?,
112+
unit: mp_rpc::v0_10_0::PriceUnitFri::Fri,
113+
})
114+
})
115+
.collect::<Result<_, _>>()?;
116+
117+
Ok(fee_estimates)
118+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
use crate::errors::StarknetRpcApiError;
2+
use crate::errors::StarknetRpcResult;
3+
use crate::Starknet;
4+
use anyhow::Context;
5+
use mc_exec::execution::TxInfo;
6+
use mc_exec::MadaraBlockViewExecutionExt;
7+
use mc_exec::EXECUTION_UNSUPPORTED_BELOW_VERSION;
8+
use mp_convert::ToFelt;
9+
use mp_rpc::v0_10_0::{BlockId, MessageFeeEstimate, MsgFromL1};
10+
use mp_transactions::L1HandlerTransaction;
11+
use starknet_api::transaction::{fields::Fee, TransactionHash};
12+
13+
/// Estimate the L2 fee of a message sent on L1
14+
///
15+
/// # Arguments
16+
///
17+
/// * `message` - the message to estimate
18+
/// * `block_id` - hash, number (height), or tag of the requested block
19+
///
20+
/// # Returns
21+
///
22+
/// * `MessageFeeEstimate` - the fee estimation (gas consumed, gas price, overall fee, unit)
23+
///
24+
/// # Errors
25+
///
26+
/// BlockNotFound : If the specified block does not exist.
27+
/// ContractNotFound : If the specified contract address does not exist.
28+
/// ContractError : If there is an error with the contract.
29+
pub async fn estimate_message_fee(
30+
starknet: &Starknet,
31+
message: MsgFromL1,
32+
block_id: BlockId,
33+
) -> StarknetRpcResult<MessageFeeEstimate> {
34+
tracing::debug!("estimate fee on block_id {block_id:?}");
35+
let view = starknet.resolve_block_view(block_id)?;
36+
let mut exec_context = view.new_execution_context()?;
37+
38+
if exec_context.protocol_version < EXECUTION_UNSUPPORTED_BELOW_VERSION {
39+
return Err(StarknetRpcApiError::unsupported_txn_version());
40+
}
41+
42+
// RPC 0.10.0: Check if the L1 handler contract exists
43+
let state_view = view.state_view();
44+
if state_view.get_contract_class_hash(&message.to_address)?.is_none() {
45+
return Err(StarknetRpcApiError::ContractNotFound {
46+
error: format!("Contract at address {:#x} not found", message.to_address).into(),
47+
});
48+
}
49+
50+
let l1_handler: L1HandlerTransaction = message.into();
51+
let tx_hash = l1_handler.compute_hash(
52+
view.backend().chain_config().chain_id.to_felt(),
53+
/* offset_version */ false,
54+
/* legacy */ false,
55+
);
56+
let tx: starknet_api::transaction::L1HandlerTransaction = l1_handler.try_into().unwrap();
57+
let transaction = blockifier::transaction::transaction_execution::Transaction::L1Handler(
58+
starknet_api::executable_transaction::L1HandlerTransaction {
59+
tx,
60+
tx_hash: TransactionHash(tx_hash),
61+
paid_fee_on_l1: Fee::default(),
62+
},
63+
);
64+
65+
let tip = transaction.tip().unwrap_or_default();
66+
// spawn_blocking: avoid starving the tokio workers during execution.
67+
let (mut execution_results, exec_context) = mp_utils::spawn_blocking(move || {
68+
Ok::<_, mc_exec::Error>((exec_context.execute_transactions([], [transaction])?, exec_context))
69+
})
70+
.await?;
71+
72+
let execution_result = execution_results.pop().context("There should be one result")?;
73+
74+
let fee_estimate = exec_context.execution_result_to_fee_estimate_v0_9(&execution_result, tip)?;
75+
76+
Ok(MessageFeeEstimate { common: fee_estimate, unit: mp_rpc::v0_10_0::PriceUnitWei::Wei })
77+
}

0 commit comments

Comments
 (0)