Skip to content
Draft
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
3 changes: 3 additions & 0 deletions madara/crates/client/db/src/view/block_preconfirmed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ impl<D: MadaraStorageRead> MadaraPreconfirmedBlockView<D> {
nonces,
deployed_contracts,
replaced_classes,
migrated_compiled_classes: vec![], // TODO(prakhar,22/11/2025): Update this
})
}

Expand Down Expand Up @@ -461,6 +462,7 @@ mod tests {
}],
old_declared_contracts: vec![Felt::from(400u64)],
replaced_classes: vec![],
migrated_compiled_classes: vec![] // TODO(prakhar,22/11/2025): Add value here and update tests
},
transactions: vec![],
events: vec![],
Expand Down Expand Up @@ -601,6 +603,7 @@ mod tests {
NonceUpdate { contract_address: Felt::from(101u64), nonce: Felt::from(1u64) }, // 0->1
NonceUpdate { contract_address: Felt::from(102u64), nonce: Felt::from(2u64) }, // 0->2
],
migrated_compiled_classes: vec![] // TODO(prakhar,22/11/2025): Add value here and update tests
}
);
}
Expand Down
1 change: 1 addition & 0 deletions madara/crates/client/devnet/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ impl ChainGenesisDescription {
deployed_contracts: self.deployed_contracts.as_state_diff(),
replaced_classes: vec![],
nonces: vec![],
migrated_compiled_classes: vec![], // TODO(prakhar,22/11/2025): Update this
},
transactions: vec![],
events: vec![],
Expand Down
65 changes: 65 additions & 0 deletions madara/crates/client/rpc/src/block_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,71 @@ impl BlockViewResolvable for mp_rpc::v0_9_0::BlockId {
}
}

// v0.10 rpc

impl StateViewResolvable for mp_rpc::v0_10_0::BlockId {
fn resolve_state_view(&self, starknet: &Starknet) -> Result<MadaraStateView, StarknetRpcApiError> {
match self {
Self::Tag(mp_rpc::v0_10_0::BlockTag::PreConfirmed) => Ok(starknet.backend.view_on_latest()),
Self::Tag(mp_rpc::v0_10_0::BlockTag::Latest) => Ok(starknet.backend.view_on_latest_confirmed()),
Self::Tag(mp_rpc::v0_10_0::BlockTag::L1Accepted) => starknet
.backend
.latest_l1_confirmed_block_n()
.and_then(|block_number| starknet.backend.view_on_confirmed(block_number))
.ok_or(StarknetRpcApiError::NoBlocks),
Self::Hash(hash) => {
if let Some(block_n) = starknet.backend.view_on_latest().find_block_by_hash(hash)? {
Ok(starknet.backend.view_on_confirmed(block_n).with_context(|| {
format!("Block with hash {hash:#x} was found at {block_n} but no such block exists")
})?)
} else {
Err(StarknetRpcApiError::BlockNotFound)
}
}
Self::Number(block_n) => {
starknet.backend.view_on_confirmed(*block_n).ok_or(StarknetRpcApiError::BlockNotFound)
}
}
}
}

impl BlockViewResolvable for mp_rpc::v0_10_0::BlockId {
fn resolve_block_view(&self, starknet: &Starknet) -> Result<MadaraBlockView, StarknetRpcApiError> {
match self {
Self::Tag(mp_rpc::v0_10_0::BlockTag::PreConfirmed) => {
Ok(starknet.backend.block_view_on_preconfirmed_or_fake()?.into())
}
Self::Tag(mp_rpc::v0_10_0::BlockTag::Latest) => {
starknet.backend.block_view_on_last_confirmed().map(|b| b.into()).ok_or(StarknetRpcApiError::NoBlocks)
}
Self::Tag(mp_rpc::v0_10_0::BlockTag::L1Accepted) => starknet
.backend
.latest_l1_confirmed_block_n()
.and_then(|block_number| starknet.backend.block_view_on_confirmed(block_number))
.map(|b| b.into())
.ok_or(StarknetRpcApiError::NoBlocks),
Self::Hash(hash) => {
if let Some(block_n) = starknet.backend.db.find_block_hash(hash)? {
Ok(starknet
.backend
.block_view_on_confirmed(block_n)
.with_context(|| {
format!("Block with hash {hash:#x} was found at {block_n} but no such block exists")
})?
.into())
} else {
Err(StarknetRpcApiError::BlockNotFound)
}
}
Self::Number(block_n) => starknet
.backend
.block_view_on_confirmed(*block_n)
.map(Into::into)
.ok_or(StarknetRpcApiError::BlockNotFound),
}
}
}

impl Starknet {
pub fn resolve_block_view<R: BlockViewResolvable>(
&self,
Expand Down
9 changes: 8 additions & 1 deletion madara/crates/client/rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//! interface for interacting with the Starknet node. This module implements the official Starknet
//! JSON-RPC specification along with some Madara-specific extensions.
//!
//! Madara fully supports the Starknet JSON-RPC specification versions `v0.7.1` and `v0.8.1`, with
//! Madara fully supports the Starknet JSON-RPC specification versions `v0.7.1`, `v0.8.1`, `v0.9.0`, and `v0.10.0`, with
//! methods accessible through port **9944** by default (configurable via `--rpc-port`). The RPC
//! server supports both HTTP and WebSocket connections on the same port.
//!
Expand All @@ -13,6 +13,8 @@
//!
//! - Default (v0.7.1): `http://localhost:9944/`
//! - Version 0.8.1: `http://localhost:9944/rpc/v0_8_1/`
//! - Version 0.9.0: `http://localhost:9944/rpc/v0_9_0/`
//! - Version 0.10.0: `http://localhost:9944/rpc/v0_10_0/`
//!
//! ## Available Endpoints
//!
Expand Down Expand Up @@ -881,6 +883,11 @@ pub fn rpc_api_user(starknet: &Starknet) -> anyhow::Result<RpcModule<()>> {
rpc_api.merge(versions::user::v0_9_0::StarknetWsRpcApiV0_9_0Server::into_rpc(starknet.clone()))?;
rpc_api.merge(versions::user::v0_9_0::StarknetTraceRpcApiV0_9_0Server::into_rpc(starknet.clone()))?;

rpc_api.merge(versions::user::v0_10_0::StarknetReadRpcApiV0_10_0Server::into_rpc(starknet.clone()))?;
rpc_api.merge(versions::user::v0_10_0::StarknetWriteRpcApiV0_10_0Server::into_rpc(starknet.clone()))?;
rpc_api.merge(versions::user::v0_10_0::StarknetWsRpcApiV0_10_0Server::into_rpc(starknet.clone()))?;
rpc_api.merge(versions::user::v0_10_0::StarknetTraceRpcApiV0_10_0Server::into_rpc(starknet.clone()))?;

Ok(rpc_api)
}

Expand Down
1 change: 1 addition & 0 deletions madara/crates/client/rpc/src/versions/user/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod v0_7_1;
pub mod v0_8_1;
pub mod v0_9_0;
pub mod v0_10_0;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pub mod read;
pub mod trace;
pub mod write;
pub mod ws;
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use crate::errors::StarknetRpcApiError;
use crate::errors::StarknetRpcResult;
use crate::Starknet;
use mc_exec::MadaraBlockViewExecutionExt;
use mc_exec::EXECUTION_UNSUPPORTED_BELOW_VERSION;
use mp_rpc::v0_10_0::{BlockId, FunctionCall};
use starknet_types_core::felt::Felt;

/// Call a Function in a Contract Without Creating a Transaction
///
/// ### Arguments
///
/// * `request` - The details of the function call to be made. This includes information such as the
/// contract address, function signature, and arguments.
/// * `block_id` - The identifier of the block used to reference the state or call the transaction
/// on. This can be the hash of the block, its number (height), or a specific block tag.
///
/// ### Returns
///
/// * `result` - The function's return value, as defined in the Cairo output. This is an array of
/// field elements (`Felt`).
///
/// ### Errors
///
/// This method may return the following errors:
/// * `CONTRACT_NOT_FOUND` - If the specified contract address does not exist.
/// * `CONTRACT_ERROR` - If there is an error with the contract or the function call.
/// * `BLOCK_NOT_FOUND` - If the specified block does not exist in the blockchain.
pub async fn call(starknet: &Starknet, request: FunctionCall, block_id: BlockId) -> StarknetRpcResult<Vec<Felt>> {
let view = starknet.resolve_block_view(block_id)?;

let mut exec_context = view.new_execution_context()?;

if exec_context.protocol_version < EXECUTION_UNSUPPORTED_BELOW_VERSION {
return Err(StarknetRpcApiError::unsupported_txn_version());
}

let FunctionCall { contract_address, entry_point_selector, calldata } = request;
// spawn_blocking: avoid starving the tokio workers during execution.
let results = mp_utils::spawn_blocking(move || {
exec_context.call_contract(&contract_address, &entry_point_selector, &calldata)
})
.await?;

Ok(results)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
use crate::errors::StarknetRpcApiError;
use crate::errors::StarknetRpcResult;
use crate::utils::tx_api_to_blockifier;
use crate::Starknet;
use blockifier::transaction::account_transaction::ExecutionFlags;
use mc_exec::execution::TxInfo;
use mc_exec::MadaraBlockViewExecutionExt;
use mc_exec::EXECUTION_UNSUPPORTED_BELOW_VERSION;
use mp_convert::ToFelt;
use mp_rpc::v0_10_0::{BlockId, BroadcastedTxn, FeeEstimate, SimulationFlagForEstimateFee};
use mp_transactions::{IntoStarknetApiExt, ToBlockifierError};
use starknet_types_core::felt::Felt;

/// Extract contract address from a broadcasted transaction for validation
fn get_contract_address_from_tx(tx: &BroadcastedTxn) -> Option<Felt> {
match tx {
BroadcastedTxn::Invoke(invoke_tx) => {
match invoke_tx {
mp_rpc::v0_10_0::BroadcastedInvokeTxn::V0(tx) => Some(tx.contract_address),
mp_rpc::v0_10_0::BroadcastedInvokeTxn::V1(tx) => Some(tx.sender_address),
mp_rpc::v0_10_0::BroadcastedInvokeTxn::V3(tx) => Some(tx.sender_address),
mp_rpc::v0_10_0::BroadcastedInvokeTxn::QueryV0(tx) => Some(tx.contract_address),
mp_rpc::v0_10_0::BroadcastedInvokeTxn::QueryV1(tx) => Some(tx.sender_address),
mp_rpc::v0_10_0::BroadcastedInvokeTxn::QueryV3(tx) => Some(tx.sender_address),
}
}
BroadcastedTxn::Declare(declare_tx) => {
match declare_tx {
mp_rpc::v0_10_0::BroadcastedDeclareTxn::V1(tx) => Some(tx.sender_address),
mp_rpc::v0_10_0::BroadcastedDeclareTxn::V2(tx) => Some(tx.sender_address),
mp_rpc::v0_10_0::BroadcastedDeclareTxn::V3(tx) => Some(tx.sender_address),
mp_rpc::v0_10_0::BroadcastedDeclareTxn::QueryV1(tx) => Some(tx.sender_address),
mp_rpc::v0_10_0::BroadcastedDeclareTxn::QueryV2(tx) => Some(tx.sender_address),
mp_rpc::v0_10_0::BroadcastedDeclareTxn::QueryV3(tx) => Some(tx.sender_address),
}
}
// DeployAccount transactions are deploying new contracts, so we don't check for existence
BroadcastedTxn::DeployAccount(_) => None,
}
}

/// Estimate the fee associated with transaction
///
/// # Arguments
///
/// * `request` - starknet transaction request
/// * `block_id` - hash of the requested block, number (height), or tag
///
/// # Returns
///
/// * `fee_estimate` - fee estimate in gwei
pub async fn estimate_fee(
starknet: &Starknet,
request: Vec<BroadcastedTxn>,
simulation_flags: Vec<SimulationFlagForEstimateFee>,
block_id: BlockId,
) -> StarknetRpcResult<Vec<FeeEstimate>> {
tracing::debug!("estimate fee on block_id {block_id:?}");
let view = starknet.resolve_block_view(block_id)?;
let mut exec_context = view.new_execution_context()?;

if exec_context.protocol_version < EXECUTION_UNSUPPORTED_BELOW_VERSION {
return Err(StarknetRpcApiError::unsupported_txn_version());
}

let validate = !simulation_flags.contains(&SimulationFlagForEstimateFee::SkipValidate);

// RPC 0.10.0: Check contract existence for transactions
let state_view = view.state_view();
for tx in &request {
if let Some(contract_address) = get_contract_address_from_tx(tx) {
if state_view.get_contract_class_hash(&contract_address)?.is_none() {
return Err(StarknetRpcApiError::ContractNotFound {
error: format!("Contract at address {:#x} not found", contract_address).into(),
});
}
}
}

let transactions = request
.into_iter()
.map(|tx| {
let only_query = tx.is_query();
let (api_tx, _) =
tx.into_starknet_api(view.backend().chain_config().chain_id.to_felt(), exec_context.protocol_version)?;
let execution_flags = ExecutionFlags { only_query, charge_fee: false, validate, strict_nonce_check: true };
Ok(tx_api_to_blockifier(api_tx, execution_flags)?)
})
.collect::<Result<Vec<_>, ToBlockifierError>>()?;

let tips = transactions.iter().map(|tx| tx.tip().unwrap_or_default()).collect::<Vec<_>>();

// spawn_blocking: avoid starving the tokio workers during execution.
let (execution_results, exec_context) = mp_utils::spawn_blocking(move || {
Ok::<_, mc_exec::Error>((exec_context.execute_transactions([], transactions)?, exec_context))
})
.await?;

let fee_estimates = execution_results
.iter()
.zip(tips)
.enumerate()
.map(|(index, (result, tip))| {
if result.execution_info.is_reverted() {
return Err(StarknetRpcApiError::TxnExecutionError {
tx_index: index,
error: result.execution_info.revert_error.as_ref().map(|e| e.to_string()).unwrap_or_default(),
});
}
Ok(FeeEstimate {
common: exec_context.execution_result_to_fee_estimate_v0_9(result, tip)?,
unit: mp_rpc::v0_10_0::PriceUnitFri::Fri,
})
})
.collect::<Result<_, _>>()?;

Ok(fee_estimates)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
use crate::errors::StarknetRpcApiError;
use crate::errors::StarknetRpcResult;
use crate::Starknet;
use anyhow::Context;
use mc_exec::execution::TxInfo;
use mc_exec::MadaraBlockViewExecutionExt;
use mc_exec::EXECUTION_UNSUPPORTED_BELOW_VERSION;
use mp_convert::ToFelt;
use mp_rpc::v0_10_0::{BlockId, MessageFeeEstimate, MsgFromL1};
use mp_transactions::L1HandlerTransaction;
use starknet_api::transaction::{fields::Fee, TransactionHash};

/// Estimate the L2 fee of a message sent on L1
///
/// # Arguments
///
/// * `message` - the message to estimate
/// * `block_id` - hash, number (height), or tag of the requested block
///
/// # Returns
///
/// * `MessageFeeEstimate` - the fee estimation (gas consumed, gas price, overall fee, unit)
///
/// # Errors
///
/// BlockNotFound : If the specified block does not exist.
/// ContractNotFound : If the specified contract address does not exist.
/// ContractError : If there is an error with the contract.
pub async fn estimate_message_fee(
starknet: &Starknet,
message: MsgFromL1,
block_id: BlockId,
) -> StarknetRpcResult<MessageFeeEstimate> {
tracing::debug!("estimate fee on block_id {block_id:?}");
let view = starknet.resolve_block_view(block_id)?;
let mut exec_context = view.new_execution_context()?;

if exec_context.protocol_version < EXECUTION_UNSUPPORTED_BELOW_VERSION {
return Err(StarknetRpcApiError::unsupported_txn_version());
}

// RPC 0.10.0: Check if the L1 handler contract exists
let state_view = view.state_view();
if state_view.get_contract_class_hash(&message.to_address)?.is_none() {
return Err(StarknetRpcApiError::ContractNotFound {
error: format!("Contract at address {:#x} not found", message.to_address).into(),
});
}

let l1_handler: L1HandlerTransaction = message.into();
let tx_hash = l1_handler.compute_hash(
view.backend().chain_config().chain_id.to_felt(),
/* offset_version */ false,
/* legacy */ false,
);
let tx: starknet_api::transaction::L1HandlerTransaction = l1_handler.try_into().unwrap();
let transaction = blockifier::transaction::transaction_execution::Transaction::L1Handler(
starknet_api::executable_transaction::L1HandlerTransaction {
tx,
tx_hash: TransactionHash(tx_hash),
paid_fee_on_l1: Fee::default(),
},
);

let tip = transaction.tip().unwrap_or_default();
// spawn_blocking: avoid starving the tokio workers during execution.
let (mut execution_results, exec_context) = mp_utils::spawn_blocking(move || {
Ok::<_, mc_exec::Error>((exec_context.execute_transactions([], [transaction])?, exec_context))
})
.await?;

let execution_result = execution_results.pop().context("There should be one result")?;

let fee_estimate = exec_context.execution_result_to_fee_estimate_v0_9(&execution_result, tip)?;

Ok(MessageFeeEstimate { common: fee_estimate, unit: mp_rpc::v0_10_0::PriceUnitWei::Wei })
}
Loading