diff --git a/madara/crates/client/db/src/view/block_preconfirmed.rs b/madara/crates/client/db/src/view/block_preconfirmed.rs index dc552b41a4..8656d25aa7 100644 --- a/madara/crates/client/db/src/view/block_preconfirmed.rs +++ b/madara/crates/client/db/src/view/block_preconfirmed.rs @@ -364,6 +364,7 @@ impl MadaraPreconfirmedBlockView { nonces, deployed_contracts, replaced_classes, + migrated_compiled_classes: vec![], // TODO(prakhar,22/11/2025): Update this }) } @@ -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![], @@ -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 } ); } diff --git a/madara/crates/client/devnet/src/lib.rs b/madara/crates/client/devnet/src/lib.rs index aa548ce2a2..4130e95124 100644 --- a/madara/crates/client/devnet/src/lib.rs +++ b/madara/crates/client/devnet/src/lib.rs @@ -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![], diff --git a/madara/crates/client/rpc/src/block_id.rs b/madara/crates/client/rpc/src/block_id.rs index 78769a5098..1df8a7406e 100644 --- a/madara/crates/client/rpc/src/block_id.rs +++ b/madara/crates/client/rpc/src/block_id.rs @@ -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 { + 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 { + 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( &self, diff --git a/madara/crates/client/rpc/src/lib.rs b/madara/crates/client/rpc/src/lib.rs index 5df5500804..cb48b3efec 100644 --- a/madara/crates/client/rpc/src/lib.rs +++ b/madara/crates/client/rpc/src/lib.rs @@ -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. //! @@ -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 //! @@ -881,6 +883,11 @@ pub fn rpc_api_user(starknet: &Starknet) -> anyhow::Result> { 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) } diff --git a/madara/crates/client/rpc/src/versions/user/mod.rs b/madara/crates/client/rpc/src/versions/user/mod.rs index b241838735..a765e1bd0a 100644 --- a/madara/crates/client/rpc/src/versions/user/mod.rs +++ b/madara/crates/client/rpc/src/versions/user/mod.rs @@ -1,3 +1,4 @@ pub mod v0_7_1; pub mod v0_8_1; pub mod v0_9_0; +pub mod v0_10_0; diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/mod.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/mod.rs new file mode 100644 index 0000000000..a4a137fd46 --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/mod.rs @@ -0,0 +1,4 @@ +pub mod read; +pub mod trace; +pub mod write; +pub mod ws; diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/call.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/call.rs new file mode 100644 index 0000000000..0b43505b00 --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/call.rs @@ -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> { + 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) +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/estimate_fee.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/estimate_fee.rs new file mode 100644 index 0000000000..125685ed14 --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/estimate_fee.rs @@ -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 { + 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, + simulation_flags: Vec, + block_id: BlockId, +) -> StarknetRpcResult> { + 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::, ToBlockifierError>>()?; + + let tips = transactions.iter().map(|tx| tx.tip().unwrap_or_default()).collect::>(); + + // 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::>()?; + + Ok(fee_estimates) +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/estimate_message_fee.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/estimate_message_fee.rs new file mode 100644 index 0000000000..ed4d377d49 --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/estimate_message_fee.rs @@ -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 { + 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 }) +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_block_transaction_count.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_block_transaction_count.rs new file mode 100644 index 0000000000..739b17d08f --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_block_transaction_count.rs @@ -0,0 +1,66 @@ +use crate::{errors::StarknetRpcResult, Starknet}; +use mp_rpc::v0_10_0::BlockId; + +/// Get the Number of Transactions in a Given Block +/// +/// ### Arguments +/// +/// * `block_id` - The identifier of the requested block. This can be the hash of the block, the +/// block's number (height), or a specific block tag. +/// +/// ### Returns +/// +/// * `transaction_count` - The number of transactions in the specified block. +/// +/// ### Errors +/// +/// This function may return a `BLOCK_NOT_FOUND` error if the specified block does not exist in +/// the blockchain. +pub fn get_block_transaction_count(starknet: &Starknet, block_id: BlockId) -> StarknetRpcResult { + let view = starknet.resolve_block_view(block_id)?; + Ok(view.get_block_info()?.tx_hashes().len() as u128) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + errors::StarknetRpcApiError, + test_utils::{sample_chain_for_block_getters, SampleChainForBlockGetters}, + }; + use mp_rpc::v0_10_0::BlockTag; + use rstest::rstest; + use starknet_types_core::felt::Felt; + + #[rstest] + fn test_get_block_transaction_count(sample_chain_for_block_getters: (SampleChainForBlockGetters, Starknet)) { + let (SampleChainForBlockGetters { block_hashes, .. }, rpc) = sample_chain_for_block_getters; + + // Block 0 + assert_eq!(get_block_transaction_count(&rpc, BlockId::Number(0)).unwrap(), 1); + assert_eq!(get_block_transaction_count(&rpc, BlockId::Hash(block_hashes[0])).unwrap(), 1); + // Block 1 + assert_eq!(get_block_transaction_count(&rpc, BlockId::Number(1)).unwrap(), 0); + assert_eq!(get_block_transaction_count(&rpc, BlockId::Hash(block_hashes[1])).unwrap(), 0); + // Block 2 + assert_eq!(get_block_transaction_count(&rpc, BlockId::Number(2)).unwrap(), 2); + assert_eq!(get_block_transaction_count(&rpc, BlockId::Hash(block_hashes[2])).unwrap(), 2); + assert_eq!(get_block_transaction_count(&rpc, BlockId::Tag(BlockTag::Latest)).unwrap(), 2); + // Pending + assert_eq!(get_block_transaction_count(&rpc, BlockId::Tag(BlockTag::PreConfirmed)).unwrap(), 1); + } + + #[rstest] + fn test_get_block_transaction_count_not_found( + sample_chain_for_block_getters: (SampleChainForBlockGetters, Starknet), + ) { + let (SampleChainForBlockGetters { .. }, rpc) = sample_chain_for_block_getters; + + assert_eq!(get_block_transaction_count(&rpc, BlockId::Number(3)), Err(StarknetRpcApiError::BlockNotFound)); + let does_not_exist = Felt::from_hex_unchecked("0x7128638126378"); + assert_eq!( + get_block_transaction_count(&rpc, BlockId::Hash(does_not_exist)), + Err(StarknetRpcApiError::BlockNotFound) + ); + } +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_block_with_receipts.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_block_with_receipts.rs new file mode 100644 index 0000000000..0658d920ed --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_block_with_receipts.rs @@ -0,0 +1,255 @@ +use crate::errors::StarknetRpcResult; +use crate::Starknet; +use mp_block::MadaraMaybePreconfirmedBlockInfo; +use mp_rpc::v0_10_0::{ + BlockId, BlockStatus, BlockWithReceipts, PreConfirmedBlockWithReceipts, StarknetGetBlockWithTxsAndReceiptsResult, + TransactionAndReceipt, +}; + +pub fn get_block_with_receipts( + starknet: &Starknet, + block_id: BlockId, +) -> StarknetRpcResult { + let view = starknet.resolve_block_view(block_id)?; + let block_info = view.get_block_info()?; + + let status = if view.is_preconfirmed() { + BlockStatus::PreConfirmed + } else if view.is_on_l1() { + BlockStatus::AcceptedOnL1 + } else { + BlockStatus::AcceptedOnL2 + }; + + let transactions_with_receipts = view + .get_executed_transactions(..)? + .into_iter() + .map(|tx| TransactionAndReceipt { + receipt: tx.receipt.to_rpc_v0_9(status.into()), + transaction: tx.transaction.to_rpc_v0_8(), + }) + .collect(); + + match block_info { + MadaraMaybePreconfirmedBlockInfo::Preconfirmed(block) => { + Ok(StarknetGetBlockWithTxsAndReceiptsResult::PreConfirmed(PreConfirmedBlockWithReceipts { + transactions: transactions_with_receipts, + pre_confirmed_block_header: block.header.to_rpc_v0_9(), + })) + } + MadaraMaybePreconfirmedBlockInfo::Confirmed(block) => { + Ok(StarknetGetBlockWithTxsAndReceiptsResult::Block(BlockWithReceipts { + transactions: transactions_with_receipts, + status, + block_header: block.to_rpc_v0_8(), + })) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + errors::StarknetRpcApiError, + test_utils::{rpc_test_setup, sample_chain_for_block_getters, SampleChainForBlockGetters}, + }; + use assert_matches::assert_matches; + use mc_db::MadaraBackend; + use mp_block::{ + header::{BlockTimestamp, GasPrices, PreconfirmedHeader}, + FullBlockWithoutCommitments, TransactionWithReceipt, + }; + use mp_chain_config::StarknetVersion; + use mp_receipt::{ + ExecutionResources, ExecutionResult, FeePayment, InvokeTransactionReceipt, PriceUnit, TransactionReceipt, + }; + use mp_rpc::v0_10_0::{BlockHeader, BlockTag, L1DaMode, PreConfirmedBlockHeader, ResourcePrice}; + use mp_transactions::{InvokeTransaction, InvokeTransactionV0, Transaction}; + use rstest::rstest; + use starknet_types_core::felt::Felt; + use std::sync::Arc; + + #[rstest] + fn test_get_block_with_receipts(sample_chain_for_block_getters: (SampleChainForBlockGetters, Starknet)) { + let (SampleChainForBlockGetters { block_hashes, expected_txs_v0_8, expected_receipts_v0_9, .. }, rpc) = + sample_chain_for_block_getters; + + // Block 0 + let res = StarknetGetBlockWithTxsAndReceiptsResult::Block(BlockWithReceipts { + status: BlockStatus::AcceptedOnL1, + transactions: vec![TransactionAndReceipt { + transaction: expected_txs_v0_8[0].transaction.clone(), + receipt: expected_receipts_v0_9[0].clone(), + }], + block_header: BlockHeader { + block_hash: block_hashes[0], + parent_hash: Felt::ZERO, + block_number: 0, + new_root: Felt::from_hex_unchecked("0x0"), + timestamp: 43, + sequencer_address: Felt::from_hex_unchecked("0xbabaa"), + l1_gas_price: ResourcePrice { price_in_fri: 12.into(), price_in_wei: 123.into() }, + l1_data_gas_price: ResourcePrice { price_in_fri: 52.into(), price_in_wei: 44.into() }, + l2_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l1_da_mode: L1DaMode::Blob, + starknet_version: "0.13.1.1".into(), + }, + }); + assert_eq!(get_block_with_receipts(&rpc, BlockId::Number(0)).unwrap(), res); + assert_eq!(get_block_with_receipts(&rpc, BlockId::Hash(block_hashes[0])).unwrap(), res); + + // Block 1 + let res = StarknetGetBlockWithTxsAndReceiptsResult::Block(BlockWithReceipts { + status: BlockStatus::AcceptedOnL2, + transactions: vec![], + block_header: BlockHeader { + block_hash: block_hashes[1], + parent_hash: block_hashes[0], + block_number: 1, + new_root: Felt::ZERO, + timestamp: 0, + sequencer_address: Felt::ZERO, + l1_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l1_data_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l2_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l1_da_mode: L1DaMode::Calldata, + starknet_version: "0.13.2".into(), + }, + }); + assert_eq!(get_block_with_receipts(&rpc, BlockId::Number(1)).unwrap(), res); + assert_eq!(get_block_with_receipts(&rpc, BlockId::Hash(block_hashes[1])).unwrap(), res); + + // Block 2 + let res = StarknetGetBlockWithTxsAndReceiptsResult::Block(BlockWithReceipts { + status: BlockStatus::AcceptedOnL2, + transactions: vec![ + TransactionAndReceipt { + transaction: expected_txs_v0_8[1].transaction.clone(), + receipt: expected_receipts_v0_9[1].clone(), + }, + TransactionAndReceipt { + transaction: expected_txs_v0_8[2].transaction.clone(), + receipt: expected_receipts_v0_9[2].clone(), + }, + ], + block_header: BlockHeader { + block_hash: block_hashes[2], + parent_hash: block_hashes[1], + block_number: 2, + new_root: Felt::ZERO, + timestamp: 0, + sequencer_address: Felt::ZERO, + l1_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l1_data_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l2_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l1_da_mode: L1DaMode::Blob, + starknet_version: "0.13.2".into(), + }, + }); + assert_eq!(get_block_with_receipts(&rpc, BlockId::Tag(BlockTag::Latest)).unwrap(), res); + assert_eq!(get_block_with_receipts(&rpc, BlockId::Number(2)).unwrap(), res); + assert_eq!(get_block_with_receipts(&rpc, BlockId::Hash(block_hashes[2])).unwrap(), res); + + // Preconfirmed + let res = StarknetGetBlockWithTxsAndReceiptsResult::PreConfirmed(PreConfirmedBlockWithReceipts { + transactions: vec![TransactionAndReceipt { + transaction: expected_txs_v0_8[3].transaction.clone(), + receipt: expected_receipts_v0_9[3].clone(), + }], + pre_confirmed_block_header: PreConfirmedBlockHeader { + block_number: 3, + timestamp: 0, + sequencer_address: Felt::ZERO, + l1_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l1_data_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l2_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l1_da_mode: L1DaMode::Blob, + starknet_version: "0.13.2".into(), + }, + }); + assert_eq!(get_block_with_receipts(&rpc, BlockId::Tag(BlockTag::PreConfirmed)).unwrap(), res); + } + + #[rstest] + fn test_get_block_with_receipts_not_found(sample_chain_for_block_getters: (SampleChainForBlockGetters, Starknet)) { + let (SampleChainForBlockGetters { .. }, rpc) = sample_chain_for_block_getters; + + assert_eq!(get_block_with_receipts(&rpc, BlockId::Number(3)), Err(StarknetRpcApiError::BlockNotFound)); + let does_not_exist = Felt::from_hex_unchecked("0x7128638126378"); + assert_eq!( + get_block_with_receipts(&rpc, BlockId::Hash(does_not_exist)), + Err(StarknetRpcApiError::BlockNotFound) + ); + } + + #[rstest] + fn test_get_block_with_receipts_pending_always_present(rpc_test_setup: (Arc, Starknet)) { + let (backend, rpc) = rpc_test_setup; + let _block_hash = backend + .write_access() + .add_full_block_with_classes( + &FullBlockWithoutCommitments { + header: PreconfirmedHeader { + block_number: 0, + sequencer_address: Felt::from_hex_unchecked("0xbabaa"), + block_timestamp: BlockTimestamp(43), + protocol_version: StarknetVersion::V0_13_1_1, + gas_prices: GasPrices { + eth_l1_gas_price: 123, + strk_l1_gas_price: 12, + eth_l1_data_gas_price: 44, + strk_l1_data_gas_price: 52, + eth_l2_gas_price: 0, + strk_l2_gas_price: 0, + }, + l1_da_mode: mp_chain_config::L1DataAvailabilityMode::Blob, + }, + state_diff: Default::default(), + transactions: vec![TransactionWithReceipt { + transaction: Transaction::Invoke(InvokeTransaction::V0(InvokeTransactionV0 { + max_fee: Felt::from_hex_unchecked("0x12"), + signature: vec![].into(), + contract_address: Felt::from_hex_unchecked("0x4343"), + entry_point_selector: Felt::from_hex_unchecked("0x1212"), + calldata: vec![Felt::from_hex_unchecked("0x2828")].into(), + })), + receipt: TransactionReceipt::Invoke(InvokeTransactionReceipt { + transaction_hash: Felt::from_hex_unchecked("0x8888888"), + actual_fee: FeePayment { amount: Felt::from_hex_unchecked("0x9"), unit: PriceUnit::Wei }, + messages_sent: vec![], + events: vec![], + execution_resources: ExecutionResources::default(), + execution_result: ExecutionResult::Succeeded, + }), + }], + events: vec![], + }, + &[], + true, + ) + .unwrap() + .block_hash; + + assert_matches!(get_block_with_receipts(&rpc, BlockId::Tag(BlockTag::PreConfirmed)).unwrap(), StarknetGetBlockWithTxsAndReceiptsResult::PreConfirmed(PreConfirmedBlockWithReceipts { + transactions, + pre_confirmed_block_header: PreConfirmedBlockHeader { + block_number: 1, + sequencer_address, + timestamp: _, + starknet_version, + l1_data_gas_price, + l1_gas_price, + l2_gas_price, + l1_da_mode: L1DaMode::Blob, + }, + }) => { + assert_eq!(transactions, vec![]); + assert_eq!(sequencer_address, Felt::from_hex_unchecked("0xbabaa")); + assert_eq!(starknet_version, StarknetVersion::V0_13_1_1.to_string()); + assert_eq!(l1_data_gas_price, ResourcePrice { price_in_fri: 52.into(), price_in_wei: 44.into() }); + assert_eq!(l1_gas_price, ResourcePrice { price_in_fri: 12.into(), price_in_wei: 123.into() }); + assert_eq!(l2_gas_price, ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }); + }); + } +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_block_with_tx_hashes.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_block_with_tx_hashes.rs new file mode 100644 index 0000000000..4205bff00d --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_block_with_tx_hashes.rs @@ -0,0 +1,159 @@ +use crate::errors::StarknetRpcResult; +use crate::Starknet; +use mp_block::MadaraMaybePreconfirmedBlockInfo; +use mp_rpc::v0_10_0::{ + BlockId, BlockStatus, BlockWithTxHashes, MaybePreConfirmedBlockWithTxHashes, PreConfirmedBlockWithTxHashes, +}; + +/// Get block information with transaction hashes given the block id. +/// +/// ### Arguments +/// +/// * `block_id` - The hash of the requested block, or number (height) of the requested block, or a +/// block tag. +/// +/// ### Returns +/// +/// Returns block information with transaction hashes. This includes either a confirmed block or +/// a pending block with transaction hashes, depending on the state of the requested block. +/// In case the block is not found, returns a `StarknetRpcApiError` with `BlockNotFound`. +pub fn get_block_with_tx_hashes( + starknet: &Starknet, + block_id: BlockId, +) -> StarknetRpcResult { + let view = starknet.resolve_block_view(block_id)?; + let block_info = view.get_block_info()?; + + let status = if view.is_preconfirmed() { + BlockStatus::PreConfirmed + } else if view.is_on_l1() { + BlockStatus::AcceptedOnL1 + } else { + BlockStatus::AcceptedOnL2 + }; + + match block_info { + MadaraMaybePreconfirmedBlockInfo::Preconfirmed(block) => { + Ok(MaybePreConfirmedBlockWithTxHashes::PreConfirmed(PreConfirmedBlockWithTxHashes { + transactions: block.tx_hashes, + pre_confirmed_block_header: block.header.to_rpc_v0_9(), + })) + } + MadaraMaybePreconfirmedBlockInfo::Confirmed(block) => { + Ok(MaybePreConfirmedBlockWithTxHashes::Block(BlockWithTxHashes { + transactions: block.tx_hashes.clone(), + status, + block_header: block.to_rpc_v0_8(), + })) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + errors::StarknetRpcApiError, + test_utils::{sample_chain_for_block_getters, SampleChainForBlockGetters}, + }; + use mp_rpc::v0_10_0::{BlockHeader, BlockTag, L1DaMode, PreConfirmedBlockHeader, ResourcePrice}; + use rstest::rstest; + use starknet_types_core::felt::Felt; + + #[rstest] + fn test_get_block_with_tx_hashes(sample_chain_for_block_getters: (SampleChainForBlockGetters, Starknet)) { + let (SampleChainForBlockGetters { block_hashes, tx_hashes, .. }, rpc) = sample_chain_for_block_getters; + + // Block 0 + let res = MaybePreConfirmedBlockWithTxHashes::Block(BlockWithTxHashes { + transactions: vec![tx_hashes[0]], + status: BlockStatus::AcceptedOnL1, + block_header: BlockHeader { + block_hash: block_hashes[0], + parent_hash: Felt::ZERO, + block_number: 0, + new_root: Felt::from_hex_unchecked("0x0"), + timestamp: 43, + sequencer_address: Felt::from_hex_unchecked("0xbabaa"), + l1_gas_price: ResourcePrice { price_in_fri: 12.into(), price_in_wei: 123.into() }, + l1_data_gas_price: ResourcePrice { price_in_fri: 52.into(), price_in_wei: 44.into() }, + l2_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l1_da_mode: L1DaMode::Blob, + starknet_version: "0.13.1.1".into(), + }, + }); + assert_eq!(get_block_with_tx_hashes(&rpc, BlockId::Number(0)).unwrap(), res); + assert_eq!(get_block_with_tx_hashes(&rpc, BlockId::Hash(block_hashes[0])).unwrap(), res); + + // Block 1 + let res = MaybePreConfirmedBlockWithTxHashes::Block(BlockWithTxHashes { + status: BlockStatus::AcceptedOnL2, + transactions: vec![], + block_header: BlockHeader { + block_hash: block_hashes[1], + parent_hash: block_hashes[0], + block_number: 1, + new_root: Felt::ZERO, + timestamp: 0, + sequencer_address: Felt::ZERO, + l1_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l1_data_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l2_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l1_da_mode: L1DaMode::Calldata, + starknet_version: "0.13.2".into(), + }, + }); + assert_eq!(get_block_with_tx_hashes(&rpc, BlockId::Number(1)).unwrap(), res); + assert_eq!(get_block_with_tx_hashes(&rpc, BlockId::Hash(block_hashes[1])).unwrap(), res); + + // Block 2 + let res = MaybePreConfirmedBlockWithTxHashes::Block(BlockWithTxHashes { + status: BlockStatus::AcceptedOnL2, + transactions: vec![tx_hashes[1], tx_hashes[2]], + block_header: BlockHeader { + block_hash: block_hashes[2], + parent_hash: block_hashes[1], + block_number: 2, + new_root: Felt::ZERO, + timestamp: 0, + sequencer_address: Felt::ZERO, + l1_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l1_data_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l2_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l1_da_mode: L1DaMode::Blob, + starknet_version: "0.13.2".into(), + }, + }); + assert_eq!(get_block_with_tx_hashes(&rpc, BlockId::Tag(BlockTag::Latest)).unwrap(), res); + assert_eq!(get_block_with_tx_hashes(&rpc, BlockId::Number(2)).unwrap(), res); + assert_eq!(get_block_with_tx_hashes(&rpc, BlockId::Hash(block_hashes[2])).unwrap(), res); + + // Pending + let res = MaybePreConfirmedBlockWithTxHashes::PreConfirmed(PreConfirmedBlockWithTxHashes { + transactions: vec![tx_hashes[3]], + pre_confirmed_block_header: PreConfirmedBlockHeader { + block_number: 3, + timestamp: 0, + sequencer_address: Felt::ZERO, + l1_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l1_data_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l2_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l1_da_mode: L1DaMode::Blob, + starknet_version: "0.13.2".into(), + }, + }); + assert_eq!(get_block_with_tx_hashes(&rpc, BlockId::Tag(BlockTag::PreConfirmed)).unwrap(), res); + } + + #[rstest] + fn test_get_block_with_tx_hashes_not_found(sample_chain_for_block_getters: (SampleChainForBlockGetters, Starknet)) { + let (SampleChainForBlockGetters { .. }, rpc) = sample_chain_for_block_getters; + + assert_eq!(get_block_with_tx_hashes(&rpc, BlockId::Number(3)), Err(StarknetRpcApiError::BlockNotFound)); + let does_not_exist = Felt::from_hex_unchecked("0x7128638126378"); + assert_eq!( + get_block_with_tx_hashes(&rpc, BlockId::Hash(does_not_exist)), + Err(StarknetRpcApiError::BlockNotFound) + ); + } +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_block_with_txs.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_block_with_txs.rs new file mode 100644 index 0000000000..9b90579831 --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_block_with_txs.rs @@ -0,0 +1,165 @@ +use crate::{Starknet, StarknetRpcResult}; +use mp_block::MadaraMaybePreconfirmedBlockInfo; +use mp_rpc::v0_10_0::{ + BlockId, BlockStatus, BlockWithTxs, MaybePreConfirmedBlockWithTxs, PreConfirmedBlockWithTxs, TxnWithHash, +}; + +/// Get block information with full transactions given the block id. +/// +/// This function retrieves detailed information about a specific block in the StarkNet network, +/// including all transactions contained within that block. The block is identified using its +/// unique block id, which can be the block's hash, its number (height), or a block tag. +/// +/// ### Arguments +/// +/// * `block_id` - The hash of the requested block, or number (height) of the requested block, or a +/// block tag. This parameter is used to specify the block from which to retrieve information and +/// transactions. +/// +/// ### Returns +/// +/// Returns detailed block information along with full transactions. Depending on the state of +/// the block, this can include either a confirmed block or a pending block with its +/// transactions. In case the specified block is not found, returns a `StarknetRpcApiError` with +/// `BlockNotFound`. +pub fn get_block_with_txs(starknet: &Starknet, block_id: BlockId) -> StarknetRpcResult { + let view = starknet.resolve_block_view(block_id)?; + let block_info = view.get_block_info()?; + + let transactions_with_hash = view + .get_executed_transactions(..)? + .into_iter() + .map(|tx| TxnWithHash { + transaction: tx.transaction.to_rpc_v0_8(), + transaction_hash: *tx.receipt.transaction_hash(), + }) + .collect(); + + let status = if view.is_preconfirmed() { + BlockStatus::PreConfirmed + } else if view.is_on_l1() { + BlockStatus::AcceptedOnL1 + } else { + BlockStatus::AcceptedOnL2 + }; + + match block_info { + MadaraMaybePreconfirmedBlockInfo::Preconfirmed(block) => { + Ok(MaybePreConfirmedBlockWithTxs::PreConfirmed(PreConfirmedBlockWithTxs { + transactions: transactions_with_hash, + pre_confirmed_block_header: block.header.to_rpc_v0_9(), + })) + } + MadaraMaybePreconfirmedBlockInfo::Confirmed(block) => Ok(MaybePreConfirmedBlockWithTxs::Block(BlockWithTxs { + transactions: transactions_with_hash, + status, + block_header: block.to_rpc_v0_8(), + })), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + errors::StarknetRpcApiError, + test_utils::{sample_chain_for_block_getters, SampleChainForBlockGetters}, + }; + use mp_rpc::v0_10_0::{BlockHeader, BlockTag, L1DaMode, PreConfirmedBlockHeader, ResourcePrice}; + use rstest::rstest; + use starknet_types_core::felt::Felt; + + #[rstest] + fn test_get_block_with_txs(sample_chain_for_block_getters: (SampleChainForBlockGetters, Starknet)) { + let (SampleChainForBlockGetters { block_hashes, expected_txs_v0_8, .. }, rpc) = sample_chain_for_block_getters; + + // Block 0 + let res = MaybePreConfirmedBlockWithTxs::Block(BlockWithTxs { + status: BlockStatus::AcceptedOnL1, + transactions: vec![expected_txs_v0_8[0].clone()], + block_header: BlockHeader { + block_hash: block_hashes[0], + parent_hash: Felt::ZERO, + block_number: 0, + new_root: Felt::from_hex_unchecked("0x0"), + timestamp: 43, + sequencer_address: Felt::from_hex_unchecked("0xbabaa"), + l1_gas_price: ResourcePrice { price_in_fri: 12.into(), price_in_wei: 123.into() }, + l1_data_gas_price: ResourcePrice { price_in_fri: 52.into(), price_in_wei: 44.into() }, + l2_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l1_da_mode: L1DaMode::Blob, + starknet_version: "0.13.1.1".into(), + }, + }); + assert_eq!(get_block_with_txs(&rpc, BlockId::Number(0)).unwrap(), res); + assert_eq!(get_block_with_txs(&rpc, BlockId::Hash(block_hashes[0])).unwrap(), res); + + // Block 1 + let res = MaybePreConfirmedBlockWithTxs::Block(BlockWithTxs { + status: BlockStatus::AcceptedOnL2, + transactions: vec![], + block_header: BlockHeader { + block_hash: block_hashes[1], + parent_hash: block_hashes[0], + block_number: 1, + new_root: Felt::ZERO, + timestamp: 0, + sequencer_address: Felt::ZERO, + l1_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l1_data_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l2_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l1_da_mode: L1DaMode::Calldata, + starknet_version: "0.13.2".into(), + }, + }); + assert_eq!(get_block_with_txs(&rpc, BlockId::Number(1)).unwrap(), res); + assert_eq!(get_block_with_txs(&rpc, BlockId::Hash(block_hashes[1])).unwrap(), res); + + // Block 2 + let res = MaybePreConfirmedBlockWithTxs::Block(BlockWithTxs { + status: BlockStatus::AcceptedOnL2, + transactions: vec![expected_txs_v0_8[1].clone(), expected_txs_v0_8[2].clone()], + block_header: BlockHeader { + block_hash: block_hashes[2], + parent_hash: block_hashes[1], + block_number: 2, + new_root: Felt::ZERO, + timestamp: 0, + sequencer_address: Felt::ZERO, + l1_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l1_data_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l2_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l1_da_mode: L1DaMode::Blob, + starknet_version: "0.13.2".into(), + }, + }); + assert_eq!(get_block_with_txs(&rpc, BlockId::Tag(BlockTag::Latest)).unwrap(), res); + assert_eq!(get_block_with_txs(&rpc, BlockId::Number(2)).unwrap(), res); + assert_eq!(get_block_with_txs(&rpc, BlockId::Hash(block_hashes[2])).unwrap(), res); + + // Pending + let res = MaybePreConfirmedBlockWithTxs::PreConfirmed(PreConfirmedBlockWithTxs { + transactions: vec![expected_txs_v0_8[3].clone()], + pre_confirmed_block_header: PreConfirmedBlockHeader { + timestamp: 0, + block_number: 3, + sequencer_address: Felt::ZERO, + l1_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l1_data_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l2_gas_price: ResourcePrice { price_in_fri: 0.into(), price_in_wei: 0.into() }, + l1_da_mode: L1DaMode::Blob, + starknet_version: "0.13.2".into(), + }, + }); + assert_eq!(get_block_with_txs(&rpc, BlockId::Tag(BlockTag::PreConfirmed)).unwrap(), res); + } + + #[rstest] + fn test_get_block_with_txs_not_found(sample_chain_for_block_getters: (SampleChainForBlockGetters, Starknet)) { + let (SampleChainForBlockGetters { .. }, rpc) = sample_chain_for_block_getters; + + assert_eq!(get_block_with_txs(&rpc, BlockId::Number(3)), Err(StarknetRpcApiError::BlockNotFound)); + let does_not_exist = Felt::from_hex_unchecked("0x7128638126378"); + assert_eq!(get_block_with_txs(&rpc, BlockId::Hash(does_not_exist)), Err(StarknetRpcApiError::BlockNotFound)); + } +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_class.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_class.rs new file mode 100644 index 0000000000..2aabba4046 --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_class.rs @@ -0,0 +1,15 @@ +use crate::errors::{StarknetRpcApiError, StarknetRpcResult}; +use crate::Starknet; +use mp_rpc::v0_10_0::{BlockId, MaybeDeprecatedContractClass}; +use starknet_types_core::felt::Felt; + +pub fn get_class( + starknet: &Starknet, + block_id: BlockId, + class_hash: Felt, +) -> StarknetRpcResult { + let view = starknet.resolve_view_on(block_id)?; + let class_info = view.get_class_info(&class_hash)?.ok_or(StarknetRpcApiError::class_hash_not_found())?; + + Ok(class_info.contract_class().into()) +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_class_at.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_class_at.rs new file mode 100644 index 0000000000..7da0938e6b --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_class_at.rs @@ -0,0 +1,37 @@ +use crate::errors::{StarknetRpcApiError, StarknetRpcResult}; +use crate::Starknet; +use anyhow::Context; +use mp_rpc::v0_10_0::{BlockId, MaybeDeprecatedContractClass}; +use starknet_types_core::felt::Felt; + +/// Get the Contract Class Definition at a Given Address in a Specific Block +/// +/// ### Arguments +/// +/// * `block_id` - The identifier of the block. This can be the hash of the block, its number +/// (height), or a specific block tag. +/// * `contract_address` - The address of the contract whose class definition will be returned. +/// +/// ### Returns +/// +/// * `contract_class` - The contract class definition. This may be either a standard contract class +/// or a deprecated contract class, depending on the contract's status and the blockchain's +/// version. +/// +/// ### Errors +/// +/// This method may return the following errors: +/// * `BLOCK_NOT_FOUND` - If the specified block does not exist in the blockchain. +/// * `CONTRACT_NOT_FOUND` - If the specified contract address does not exist. +pub fn get_class_at( + starknet: &Starknet, + block_id: BlockId, + contract_address: Felt, +) -> StarknetRpcResult { + let view = starknet.resolve_view_on(block_id)?; + let class_hash = + view.get_contract_class_hash(&contract_address)?.ok_or(StarknetRpcApiError::contract_not_found())?; + let class_info = view.get_class_info(&class_hash)?.context("Class info should exist")?; + + Ok(class_info.contract_class().into()) +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_class_hash_at.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_class_hash_at.rs new file mode 100644 index 0000000000..f31f022630 --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_class_hash_at.rs @@ -0,0 +1,78 @@ +use crate::errors::{StarknetRpcApiError, StarknetRpcResult}; +use crate::Starknet; +use mp_rpc::v0_10_0::BlockId; +use starknet_types_core::felt::Felt; + +/// Get the contract class hash in the given block for the contract deployed at the given +/// address +/// +/// ### Arguments +/// +/// * `block_id` - The hash of the requested block, or number (height) of the requested block, or a +/// block tag +/// * `contract_address` - The address of the contract whose class hash will be returned +/// +/// ### Returns +/// +/// * `class_hash` - The class hash of the given contract +pub fn get_class_hash_at(starknet: &Starknet, block_id: BlockId, contract_address: Felt) -> StarknetRpcResult { + let view = starknet.resolve_view_on(block_id)?; + tracing::debug!("{view:?}"); + view.get_contract_class_hash(&contract_address)?.ok_or(StarknetRpcApiError::contract_not_found()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::{sample_chain_for_state_updates, SampleChainForStateUpdates}; + use mp_rpc::v0_10_0::BlockTag; + use rstest::rstest; + + #[rstest] + fn test_get_class_hash_at(sample_chain_for_state_updates: (SampleChainForStateUpdates, Starknet)) { + let (SampleChainForStateUpdates { contracts, class_hashes, .. }, rpc) = sample_chain_for_state_updates; + + // Block 0 + let block_n = BlockId::Number(0); + assert_eq!(get_class_hash_at(&rpc, block_n.clone(), contracts[0]).unwrap(), class_hashes[0]); + assert_eq!( + get_class_hash_at(&rpc, block_n.clone(), contracts[1]), + Err(StarknetRpcApiError::contract_not_found()) + ); + assert_eq!(get_class_hash_at(&rpc, block_n, contracts[2]), Err(StarknetRpcApiError::contract_not_found())); + + // Block 1 + let block_n = BlockId::Number(1); + assert_eq!(get_class_hash_at(&rpc, block_n.clone(), contracts[0]).unwrap(), class_hashes[0]); + assert_eq!(get_class_hash_at(&rpc, block_n.clone(), contracts[1]).unwrap(), class_hashes[1]); + assert_eq!(get_class_hash_at(&rpc, block_n, contracts[2]).unwrap(), class_hashes[0]); + + // Block 2 + let block_n = BlockId::Number(2); + assert_eq!(get_class_hash_at(&rpc, block_n.clone(), contracts[0]).unwrap(), class_hashes[0]); + assert_eq!(get_class_hash_at(&rpc, block_n.clone(), contracts[1]).unwrap(), class_hashes[1]); + assert_eq!(get_class_hash_at(&rpc, block_n, contracts[2]).unwrap(), class_hashes[0]); + + // Pending + let block_n = BlockId::Tag(BlockTag::PreConfirmed); + assert_eq!(get_class_hash_at(&rpc, block_n.clone(), contracts[0]).unwrap(), class_hashes[2]); + assert_eq!(get_class_hash_at(&rpc, block_n.clone(), contracts[1]).unwrap(), class_hashes[1]); + assert_eq!(get_class_hash_at(&rpc, block_n, contracts[2]).unwrap(), class_hashes[0]); + } + + #[rstest] + fn test_get_class_hash_at_not_found(sample_chain_for_state_updates: (SampleChainForStateUpdates, Starknet)) { + let (SampleChainForStateUpdates { contracts, .. }, rpc) = sample_chain_for_state_updates; + + // Not found + let block_n = BlockId::Number(3); + assert_eq!(get_class_hash_at(&rpc, block_n, contracts[0]), Err(StarknetRpcApiError::BlockNotFound)); + let block_n = BlockId::Number(0); + assert_eq!( + get_class_hash_at(&rpc, block_n.clone(), contracts[1]), + Err(StarknetRpcApiError::contract_not_found()) + ); + let does_not_exist = Felt::from_hex_unchecked("0x7128638126378"); + assert_eq!(get_class_hash_at(&rpc, block_n, does_not_exist), Err(StarknetRpcApiError::contract_not_found())); + } +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_events.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_events.rs new file mode 100644 index 0000000000..d768f79d38 --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_events.rs @@ -0,0 +1,90 @@ +use crate::constants::{MAX_EVENTS_CHUNK_SIZE, MAX_EVENTS_KEYS}; +use crate::errors::{StarknetRpcApiError, StarknetRpcResult}; +use crate::types::ContinuationToken; +use crate::Starknet; +use anyhow::Context; +use mc_db::EventFilter; +use mp_block::EventWithInfo; +use mp_rpc::v0_10_0::{EventFilterWithPageRequest, EventsChunk}; + +/// Returns all events matching the given filter. +/// +/// This function retrieves all event objects that match the conditions specified in the +/// provided event filter. The filter can include various criteria such as contract addresses, +/// event types, and block ranges. The function supports pagination through the result page +/// request schema. +/// +/// ### Arguments +/// +/// * `filter` - The conditions used to filter the returned events. The filter is a combination of +/// an event filter and a result page request, allowing for precise control over which events are +/// returned and in what quantity. +/// +/// ### Returns +/// +/// Returns a chunk of event objects that match the filter criteria, encapsulated in an +/// `EventsChunk` type. The chunk includes details about the events, such as their data, the +/// block in which they occurred, and the transaction that triggered them. In case of +/// errors, such as `PAGE_SIZE_TOO_BIG`, `INVALID_CONTINUATION_TOKEN`, `BLOCK_NOT_FOUND`, or +/// `TOO_MANY_KEYS_IN_FILTER`, returns a `StarknetRpcApiError` indicating the specific issue. +pub fn get_events(starknet: &Starknet, filter: EventFilterWithPageRequest) -> StarknetRpcResult { + let from_address = filter.address; + let keys = filter.keys; + let chunk_size = filter.chunk_size as usize; + + let view = starknet.backend.view_on_latest(); + + if keys.as_ref().map(|k| k.iter().map(|pattern| pattern.len()).sum()).unwrap_or(0) > MAX_EVENTS_KEYS { + return Err(StarknetRpcApiError::TooManyKeysInFilter); + } + if chunk_size > MAX_EVENTS_CHUNK_SIZE { + return Err(StarknetRpcApiError::PageSizeTooBig); + } + + // Get the block numbers for the requested range + + let from_block_n = match filter.from_block { + Some(block_id) => starknet.resolve_view_on(block_id)?.latest_block_n().unwrap_or(0), + None => 0, + }; + let to_block_n = match filter.to_block { + Some(block_id) => starknet.resolve_view_on(block_id)?.latest_block_n().unwrap_or(0), + None => view.latest_block_n().unwrap_or(0), + }; + + let continuation_token = match filter.continuation_token { + Some(token) => ContinuationToken::parse(token).map_err(|_| StarknetRpcApiError::InvalidContinuationToken)?, + None => ContinuationToken { block_number: from_block_n, event_n: 0 }, + }; + + // Verify that the requested range is valid + if from_block_n > to_block_n { + return Ok(EventsChunk { events: vec![], continuation_token: None }); + } + + let from_block = continuation_token.block_number; + let from_event_n = continuation_token.event_n as usize; + + let mut events_infos = view + .get_events(EventFilter { + start_block: from_block, + start_event_index: from_event_n, + end_block: to_block_n, + from_address, + keys_pattern: keys, + max_events: chunk_size + 1, + }) + .context("Error getting filtered events")?; + + let mut continuation_token = None; + if events_infos.len() > chunk_size { + continuation_token = events_infos.pop().map(|EventWithInfo { block_number, event_index_in_block, .. }| { + ContinuationToken { block_number, event_n: (event_index_in_block + 1) } + }); + } + + Ok(EventsChunk { + events: events_infos.into_iter().map(|event_info| event_info.into()).collect(), + continuation_token: continuation_token.map(|token| token.to_string()), + }) +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_nonce.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_nonce.rs new file mode 100644 index 0000000000..0c157a2dd8 --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_nonce.rs @@ -0,0 +1,80 @@ +use crate::errors::{StarknetRpcApiError, StarknetRpcResult}; +use crate::Starknet; +use mp_rpc::v0_10_0::BlockId; +use starknet_types_core::felt::Felt; + +/// Get the nonce associated with the given address in the given block. +/// +/// ### Arguments +/// +/// * `block_id` - The hash of the requested block, or number (height) of the requested block, or a +/// block tag. This parameter specifies the block in which the nonce is to be checked. +/// * `contract_address` - The address of the contract whose nonce we're seeking. This is the unique +/// identifier of the contract in the Starknet network. +/// +/// ### Returns +/// +/// Returns the contract's nonce at the requested state. The nonce is returned as a +/// `Felt`, representing the current state of the contract in terms of transactions +/// count or other contract-specific operations. In case of errors, such as +/// `BLOCK_NOT_FOUND` or `CONTRACT_NOT_FOUND`, returns a `StarknetRpcApiError` indicating the +/// specific issue. +pub fn get_nonce(starknet: &Starknet, block_id: BlockId, contract_address: Felt) -> StarknetRpcResult { + let view = starknet.resolve_view_on(block_id)?; + + if !view.is_contract_deployed(&contract_address)? { + return Err(StarknetRpcApiError::contract_not_found()); + } + + Ok(view.get_contract_nonce(&contract_address)?.unwrap_or(Felt::ZERO)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::{sample_chain_for_state_updates, SampleChainForStateUpdates}; + use mp_rpc::v0_10_0::BlockTag; + use rstest::rstest; + + #[rstest] + fn test_get_nonce(sample_chain_for_state_updates: (SampleChainForStateUpdates, Starknet)) { + let (SampleChainForStateUpdates { contracts, .. }, rpc) = sample_chain_for_state_updates; + + // Block 0 + let block_n = BlockId::Number(0); + assert_eq!(get_nonce(&rpc, block_n.clone(), contracts[0]).unwrap(), 0.into()); + assert_eq!(get_nonce(&rpc, block_n.clone(), contracts[1]), Err(StarknetRpcApiError::contract_not_found())); + assert_eq!(get_nonce(&rpc, block_n, contracts[2]), Err(StarknetRpcApiError::contract_not_found())); + + // Block 1 + let block_n = BlockId::Number(1); + assert_eq!(get_nonce(&rpc, block_n.clone(), contracts[0]).unwrap(), 1.into()); + assert_eq!(get_nonce(&rpc, block_n.clone(), contracts[1]).unwrap(), 0.into()); + assert_eq!(get_nonce(&rpc, block_n, contracts[2]).unwrap(), 2.into()); + + // Block 2 + let block_n = BlockId::Number(2); + assert_eq!(get_nonce(&rpc, block_n.clone(), contracts[0]).unwrap(), 1.into()); + assert_eq!(get_nonce(&rpc, block_n.clone(), contracts[1]).unwrap(), 0.into()); + assert_eq!(get_nonce(&rpc, block_n, contracts[2]).unwrap(), 2.into()); + + // Pending + let block_n = BlockId::Tag(BlockTag::PreConfirmed); + assert_eq!(get_nonce(&rpc, block_n.clone(), contracts[0]).unwrap(), 3.into()); + assert_eq!(get_nonce(&rpc, block_n.clone(), contracts[1]).unwrap(), 2.into()); + assert_eq!(get_nonce(&rpc, block_n, contracts[2]).unwrap(), 2.into()); + } + + #[rstest] + fn test_get_nonce_not_found(sample_chain_for_state_updates: (SampleChainForStateUpdates, Starknet)) { + let (SampleChainForStateUpdates { contracts, .. }, rpc) = sample_chain_for_state_updates; + + // Not found + let block_n = BlockId::Number(3); + assert_eq!(get_nonce(&rpc, block_n, contracts[0]), Err(StarknetRpcApiError::BlockNotFound)); + let block_n = BlockId::Number(0); + assert_eq!(get_nonce(&rpc, block_n.clone(), contracts[1]), Err(StarknetRpcApiError::contract_not_found())); + let does_not_exist = Felt::from_hex_unchecked("0x7128638126378"); + assert_eq!(get_nonce(&rpc, block_n, does_not_exist), Err(StarknetRpcApiError::contract_not_found())); + } +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_state_update.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_state_update.rs new file mode 100644 index 0000000000..73324b720c --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_state_update.rs @@ -0,0 +1,110 @@ +use crate::errors::StarknetRpcResult; +use crate::Starknet; +use mp_rpc::v0_10_0::{BlockId, MaybePreConfirmedStateUpdate, PreConfirmedStateUpdate, StateUpdate}; +use starknet_types_core::felt::Felt; + +/// Get the information about the result of executing the requested block. +/// +/// This function fetches details about the state update resulting from executing a specific +/// block in the StarkNet network. The block is identified using its unique block id, which can +/// be the block's hash, its number (height), or a block tag. +/// +/// ### Arguments +/// +/// * `block_id` - The hash of the requested block, or number (height) of the requested block, or a +/// block tag. This parameter specifies the block for which the state update information is +/// required. +/// +/// ### Returns +/// +/// Returns information about the state update of the requested block, including any changes to +/// the state of the network as a result of the block's execution. This can include a confirmed +/// state update or a pending state update. If the block is not found, returns a +/// `StarknetRpcApiError` with `BlockNotFound`. +pub fn get_state_update(starknet: &Starknet, block_id: BlockId) -> StarknetRpcResult { + let view = starknet.resolve_block_view(block_id)?; + let state_diff = view.get_state_diff()?; + let old_root = if let Some(parent) = view.parent_block() { + parent.get_block_info()?.header.global_state_root + } else { + Felt::ZERO + }; + + if let Some(confirmed) = view.as_confirmed() { + let block_info = confirmed.get_block_info()?; + Ok(MaybePreConfirmedStateUpdate::Block(StateUpdate { + block_hash: block_info.block_hash, + old_root, + new_root: block_info.header.global_state_root, + state_diff: state_diff.into(), + })) + } else { + Ok(MaybePreConfirmedStateUpdate::PreConfirmed(PreConfirmedStateUpdate { + state_diff: state_diff.into(), + })) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + test_utils::{sample_chain_for_state_updates, SampleChainForStateUpdates}, + StarknetRpcApiError, + }; + use mp_rpc::v0_10_0::BlockTag; + use rstest::rstest; + + #[rstest] + fn test_get_state_update(sample_chain_for_state_updates: (SampleChainForStateUpdates, Starknet)) { + let (SampleChainForStateUpdates { block_hashes, state_roots, state_diffs, .. }, rpc) = + sample_chain_for_state_updates; + + // Block 0 + let res = MaybePreConfirmedStateUpdate::Block(StateUpdate { + block_hash: block_hashes[0], + old_root: Felt::ZERO, + new_root: state_roots[0], + state_diff: state_diffs[0].clone().into(), + }); + assert_eq!(get_state_update(&rpc, BlockId::Number(0)).unwrap(), res); + assert_eq!(get_state_update(&rpc, BlockId::Hash(block_hashes[0])).unwrap(), res); + + // Block 1 + let res = MaybePreConfirmedStateUpdate::Block(StateUpdate { + block_hash: block_hashes[1], + old_root: state_roots[0], + new_root: state_roots[1], + state_diff: state_diffs[1].clone().into(), + }); + assert_eq!(get_state_update(&rpc, BlockId::Number(1)).unwrap(), res); + assert_eq!(get_state_update(&rpc, BlockId::Hash(block_hashes[1])).unwrap(), res); + + // Block 2 + let res = MaybePreConfirmedStateUpdate::Block(StateUpdate { + block_hash: block_hashes[2], + old_root: state_roots[1], + new_root: state_roots[2], + state_diff: state_diffs[2].clone().into(), + }); + assert_eq!(get_state_update(&rpc, BlockId::Number(2)).unwrap(), res); + assert_eq!(get_state_update(&rpc, BlockId::Hash(block_hashes[2])).unwrap(), res); + assert_eq!(get_state_update(&rpc, BlockId::Tag(BlockTag::Latest)).unwrap(), res); + + // Pending + let res = MaybePreConfirmedStateUpdate::PreConfirmed(PreConfirmedStateUpdate { + state_diff: state_diffs[3].clone().into(), + }); + assert_eq!(get_state_update(&rpc, BlockId::Tag(BlockTag::PreConfirmed)).unwrap(), res); + } + + #[rstest] + + fn test_get_state_update_not_found(sample_chain_for_state_updates: (SampleChainForStateUpdates, Starknet)) { + let (SampleChainForStateUpdates { .. }, rpc) = sample_chain_for_state_updates; + + assert_eq!(get_state_update(&rpc, BlockId::Number(3)), Err(StarknetRpcApiError::BlockNotFound)); + let does_not_exist = Felt::from_hex_unchecked("0x7128638126378"); + assert_eq!(get_state_update(&rpc, BlockId::Hash(does_not_exist)), Err(StarknetRpcApiError::BlockNotFound)); + } +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_storage_at.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_storage_at.rs new file mode 100644 index 0000000000..46d2c1916e --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_storage_at.rs @@ -0,0 +1,140 @@ +use crate::errors::{StarknetRpcApiError, StarknetRpcResult}; +use crate::Starknet; +use mp_rpc::v0_10_0::BlockId; +use starknet_types_core::felt::Felt; + +/// Get the value of the storage at the given address and key. +/// +/// This function retrieves the value stored in a specified contract's storage, identified by a +/// contract address and a storage key, within a specified block in the current network. +/// +/// ### Arguments +/// +/// * `contract_address` - The address of the contract to read from. This parameter identifies the +/// contract whose storage is being queried. +/// * `key` - The key to the storage value for the given contract. This parameter specifies the +/// particular storage slot to be queried. +/// * `block_id` - The hash of the requested block, or number (height) of the requested block, or a +/// block tag. This parameter defines the state of the blockchain at which the storage value is to +/// be read. +/// +/// ### Returns +/// +/// Returns the value at the given key for the given contract, represented as a `Felt`. +/// If no value is found at the specified storage key, returns 0. +/// +/// ### Errors +/// +/// This function may return errors in the following cases: +/// +/// * `BLOCK_NOT_FOUND` - If the specified block does not exist in the blockchain. +/// * `CONTRACT_NOT_FOUND` - If the specified contract does not exist or is not deployed at the +/// given `contract_address` in the specified block. +pub fn get_storage_at( + starknet: &Starknet, + contract_address: Felt, + key: Felt, + block_id: BlockId, +) -> StarknetRpcResult { + let view = starknet.resolve_view_on(block_id)?; + + // Felt::ONE is a special contract address that is a mapping of the block number to the block hash. + // no contract is deployed at this address, so we skip the contract check. + if contract_address != Felt::ONE && !view.is_contract_deployed(&contract_address)? { + return Err(StarknetRpcApiError::contract_not_found()); + } + + Ok(view.get_contract_storage(&contract_address, &key)?.unwrap_or(Felt::ZERO)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::{sample_chain_for_state_updates, SampleChainForStateUpdates}; + use mp_rpc::v0_10_0::BlockTag; + use rstest::rstest; + + #[rstest] + fn test_get_storage_at(sample_chain_for_state_updates: (SampleChainForStateUpdates, Starknet)) { + let (SampleChainForStateUpdates { keys, values, contracts, .. }, rpc) = sample_chain_for_state_updates; + + // Expected values are in the format `values[contract][key] = value`. + let check_contract_key_value = |block_n: BlockId, contracts_kv: [Option<[Felt; 3]>; 3]| { + for (contract_i, contract_values) in contracts_kv.into_iter().enumerate() { + if let Some(contract_values) = contract_values { + for (key_i, value) in contract_values.into_iter().enumerate() { + assert_eq!( + get_storage_at(&rpc, contracts[contract_i], keys[key_i], block_n.clone()).unwrap(), + value, + "get storage at blockid {block_n:?}, contract #{contract_i}, key #{key_i}" + ); + } + } else { + // contract not found + for (key_i, _) in keys.iter().enumerate() { + assert_eq!( + get_storage_at(&rpc, contracts[contract_i], keys[key_i], block_n.clone()), + Err(StarknetRpcApiError::contract_not_found()), + "get storage at blockid {block_n:?}, contract #{contract_i}, key #{key_i} should not found" + ); + } + } + } + }; + + // Block 0 + let block_n = BlockId::Number(0); + let expected = [Some([values[0], Felt::ZERO, values[2]]), None, None]; + check_contract_key_value(block_n, expected); + + // Block 1 + let block_n = BlockId::Number(1); + let expected = [ + Some([values[1], Felt::ZERO, values[2]]), + Some([Felt::ZERO, Felt::ZERO, Felt::ZERO]), + Some([Felt::ZERO, Felt::ZERO, values[0]]), + ]; + check_contract_key_value(block_n, expected); + + // Block 2 + let block_n = BlockId::Number(2); + let expected = [ + Some([values[1], Felt::ZERO, values[2]]), + Some([values[0], Felt::ZERO, Felt::ZERO]), + Some([Felt::ZERO, values[2], values[0]]), + ]; + check_contract_key_value(block_n, expected); + + // Pending + let block_n = BlockId::Tag(BlockTag::PreConfirmed); + let expected = [ + Some([values[2], values[0], values[2]]), + Some([values[0], Felt::ZERO, Felt::ZERO]), + Some([Felt::ZERO, values[2], values[0]]), + ]; + check_contract_key_value(block_n, expected); + } + + #[rstest] + fn test_get_storage_at_not_found(sample_chain_for_state_updates: (SampleChainForStateUpdates, Starknet)) { + let (SampleChainForStateUpdates { keys, contracts, .. }, rpc) = sample_chain_for_state_updates; + + // Not found + let block_n = BlockId::Number(3); + assert_eq!(get_storage_at(&rpc, contracts[0], keys[0], block_n), Err(StarknetRpcApiError::BlockNotFound)); + let block_n = BlockId::Number(0); + assert_eq!( + get_storage_at(&rpc, contracts[1], keys[0], block_n.clone()), + Err(StarknetRpcApiError::contract_not_found()) + ); + let does_not_exist = Felt::from_hex_unchecked("0x7128638126378"); + assert_eq!( + get_storage_at(&rpc, does_not_exist, keys[0], block_n.clone()), + Err(StarknetRpcApiError::contract_not_found()) + ); + assert_eq!( + get_storage_at(&rpc, contracts[0], keys[1], block_n), + Ok(Felt::ZERO) // return ZERO when key not found + ); + } +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_transaction_by_block_id_and_index.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_transaction_by_block_id_and_index.rs new file mode 100644 index 0000000000..54570cfe39 --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_transaction_by_block_id_and_index.rs @@ -0,0 +1,101 @@ +use crate::errors::{StarknetRpcApiError, StarknetRpcResult}; +use crate::Starknet; +use mp_rpc::v0_10_0::{BlockId, TxnWithHash}; + +/// Get the details of a transaction by a given block id and index. +/// +/// This function fetches the details of a specific transaction in the StarkNet network by +/// identifying it through its block and position (index) within that block. If no transaction +/// is found at the specified index, null is returned. +/// +/// ### Arguments +/// +/// * `block_id` - The hash of the requested block, or number (height) of the requested block, or a +/// block tag. This parameter is used to specify the block in which the transaction is located. +/// * `index` - An integer representing the index in the block where the transaction is expected to +/// be found. The index starts from 0 and increases sequentially for each transaction in the +/// block. +/// +/// ### Returns +/// +/// Returns the details of the transaction if found, including the transaction hash. The +/// transaction details are returned as a type conforming to the StarkNet protocol. In case of +/// errors like `BLOCK_NOT_FOUND` or `INVALID_TXN_INDEX`, returns a `StarknetRpcApiError` +/// indicating the specific issue. +pub fn get_transaction_by_block_id_and_index( + starknet: &Starknet, + block_id: BlockId, + index: u64, +) -> StarknetRpcResult { + let view = starknet.resolve_block_view(block_id)?; + let tx = view.get_executed_transaction(index)?.ok_or(StarknetRpcApiError::InvalidTxnIndex)?; + + Ok(TxnWithHash { transaction: tx.transaction.to_rpc_v0_8(), transaction_hash: *tx.receipt.transaction_hash() }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::{sample_chain_for_block_getters, SampleChainForBlockGetters}; + use mp_rpc::v0_10_0::BlockTag; + use rstest::rstest; + + #[rstest] + fn test_get_transaction_by_block_id_and_index( + sample_chain_for_block_getters: (SampleChainForBlockGetters, Starknet), + ) { + let (SampleChainForBlockGetters { block_hashes, expected_txs_v0_8, .. }, rpc) = sample_chain_for_block_getters; + + // Block 0 + assert_eq!(get_transaction_by_block_id_and_index(&rpc, BlockId::Number(0), 0).unwrap(), expected_txs_v0_8[0]); + assert_eq!( + get_transaction_by_block_id_and_index(&rpc, BlockId::Hash(block_hashes[0]), 0).unwrap(), + expected_txs_v0_8[0] + ); + + // Block 1 + + // Block 2 + assert_eq!(get_transaction_by_block_id_and_index(&rpc, BlockId::Number(2), 0).unwrap(), expected_txs_v0_8[1]); + assert_eq!( + get_transaction_by_block_id_and_index(&rpc, BlockId::Hash(block_hashes[2]), 0).unwrap(), + expected_txs_v0_8[1] + ); + assert_eq!( + get_transaction_by_block_id_and_index(&rpc, BlockId::Tag(BlockTag::Latest), 0).unwrap(), + expected_txs_v0_8[1] + ); + + assert_eq!(get_transaction_by_block_id_and_index(&rpc, BlockId::Number(2), 1).unwrap(), expected_txs_v0_8[2]); + assert_eq!( + get_transaction_by_block_id_and_index(&rpc, BlockId::Hash(block_hashes[2]), 1).unwrap(), + expected_txs_v0_8[2] + ); + assert_eq!( + get_transaction_by_block_id_and_index(&rpc, BlockId::Tag(BlockTag::Latest), 1).unwrap(), + expected_txs_v0_8[2] + ); + + // Pending + assert_eq!( + get_transaction_by_block_id_and_index(&rpc, BlockId::Tag(BlockTag::PreConfirmed), 0).unwrap(), + expected_txs_v0_8[3] + ); + } + + #[rstest] + fn test_get_transaction_by_block_id_and_index_not_found( + sample_chain_for_block_getters: (SampleChainForBlockGetters, Starknet), + ) { + let (SampleChainForBlockGetters { .. }, rpc) = sample_chain_for_block_getters; + + assert_eq!( + get_transaction_by_block_id_and_index(&rpc, BlockId::Number(4), 0), + Err(StarknetRpcApiError::BlockNotFound) + ); + assert_eq!( + get_transaction_by_block_id_and_index(&rpc, BlockId::Number(0), 1), + Err(StarknetRpcApiError::InvalidTxnIndex) + ); + } +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_transaction_by_hash.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_transaction_by_hash.rs new file mode 100644 index 0000000000..5dd5157204 --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_transaction_by_hash.rs @@ -0,0 +1,66 @@ +use crate::errors::{StarknetRpcApiError, StarknetRpcResult}; +use crate::Starknet; +use mp_rpc::v0_10_0::TxnWithHash; +use starknet_types_core::felt::Felt; + +/// Get the details and status of a submitted transaction. +/// +/// This function retrieves the detailed information and status of a transaction identified by +/// its hash. The transaction hash uniquely identifies a specific transaction that has been +/// submitted to the StarkNet network. +/// +/// ### Arguments +/// +/// * `transaction_hash` - The hash of the requested transaction. This parameter specifies the +/// transaction for which details and status are requested. +/// +/// ### Returns +/// +/// Returns information about the requested transaction, including its status, sender, +/// recipient, and other transaction details. The information is encapsulated in a `Transaction` +/// type, which is a combination of the `TXN` schema and additional properties, such as the +/// `transaction_hash`. In case the specified transaction hash is not found, returns a +/// `StarknetRpcApiError` with `TXN_HASH_NOT_FOUND`. +/// +/// ### Errors +/// +/// The function may return one of the following errors if encountered: +/// - `TXN_HASH_NOT_FOUND` if the specified transaction is not found. +pub fn get_transaction_by_hash(starknet: &Starknet, transaction_hash: Felt) -> StarknetRpcResult { + let view = starknet.backend.view_on_latest(); + let res = view.find_transaction_by_hash(&transaction_hash)?.ok_or(StarknetRpcApiError::TxnHashNotFound)?; + let transaction = res.get_transaction()?; + Ok(TxnWithHash { transaction: transaction.transaction.to_rpc_v0_8(), transaction_hash }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::{sample_chain_for_block_getters, SampleChainForBlockGetters}; + use rstest::rstest; + + #[rstest] + fn test_get_transaction_by_hash(sample_chain_for_block_getters: (SampleChainForBlockGetters, Starknet)) { + let (SampleChainForBlockGetters { tx_hashes, expected_txs_v0_8, .. }, rpc) = sample_chain_for_block_getters; + + // Block 0 + assert_eq!(get_transaction_by_hash(&rpc, tx_hashes[0]).unwrap(), expected_txs_v0_8[0]); + + // Block 1 + + // Block 2 + assert_eq!(get_transaction_by_hash(&rpc, tx_hashes[1]).unwrap(), expected_txs_v0_8[1]); + assert_eq!(get_transaction_by_hash(&rpc, tx_hashes[2]).unwrap(), expected_txs_v0_8[2]); + + // Pending + assert_eq!(get_transaction_by_hash(&rpc, tx_hashes[3]).unwrap(), expected_txs_v0_8[3]); + } + + #[rstest] + fn test_get_transaction_by_hash_not_found(sample_chain_for_block_getters: (SampleChainForBlockGetters, Starknet)) { + let (SampleChainForBlockGetters { .. }, rpc) = sample_chain_for_block_getters; + + let does_not_exist = Felt::from_hex_unchecked("0x7128638126378"); + assert_eq!(get_transaction_by_hash(&rpc, does_not_exist), Err(StarknetRpcApiError::TxnHashNotFound)); + } +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_transaction_receipt.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_transaction_receipt.rs new file mode 100644 index 0000000000..9204a2a2b0 --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_transaction_receipt.rs @@ -0,0 +1,109 @@ +use crate::errors::{StarknetRpcApiError, StarknetRpcResult}; +use crate::Starknet; +use mp_rpc::v0_10_0::{BlockStatus, TxnReceiptWithBlockInfo}; +use starknet_types_core::felt::Felt; + +/// Get the transaction receipt by the transaction hash. +/// +/// This function retrieves the transaction receipt for a specific transaction identified by its +/// hash. The transaction receipt includes information about the execution status of the +/// transaction, events generated during its execution, and other relevant details. +/// +/// ### Arguments +/// +/// * `transaction_hash` - The hash of the requested transaction. This parameter specifies the +/// transaction for which the receipt is requested. +/// +/// ### Returns +/// +/// Returns a transaction receipt, which can be one of two types: +/// - `TransactionReceipt` if the transaction has been processed and has a receipt. +/// - `PendingTransactionReceipt` if the transaction is pending and the receipt is not yet +/// available. +/// +/// ### Errors +/// +/// The function may return a `TXN_HASH_NOT_FOUND` error if the specified transaction hash is +/// not found. +pub fn get_transaction_receipt( + starknet: &Starknet, + transaction_hash: Felt, +) -> StarknetRpcResult { + tracing::debug!("get tx receipt {transaction_hash:#x}"); + let view = starknet.backend.view_on_latest(); + let res = view.find_transaction_by_hash(&transaction_hash)?.ok_or(StarknetRpcApiError::TxnHashNotFound)?; + let transaction = res.get_transaction()?; + + let status = if res.block.is_preconfirmed() { + BlockStatus::PreConfirmed + } else if res.block.is_on_l1() { + BlockStatus::AcceptedOnL1 + } else { + BlockStatus::AcceptedOnL2 + }; + let transaction_receipt = transaction.receipt.clone().to_rpc_v0_9(status.into()); + + if let Some(confirmed) = res.block.as_confirmed() { + let block_hash = confirmed.get_block_info()?.block_hash; + Ok(TxnReceiptWithBlockInfo { + transaction_receipt, + block_hash: Some(block_hash), + block_number: res.block.block_number(), + }) + } else { + Ok(TxnReceiptWithBlockInfo { transaction_receipt, block_hash: None, block_number: res.block.block_number() }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::{sample_chain_for_block_getters, SampleChainForBlockGetters}; + use rstest::rstest; + + #[rstest] + fn test_get_transaction_receipt(sample_chain_for_block_getters: (SampleChainForBlockGetters, Starknet)) { + let (SampleChainForBlockGetters { block_hashes, tx_hashes, expected_receipts_v0_9, .. }, rpc) = + sample_chain_for_block_getters; + + // Block 0 + let res = TxnReceiptWithBlockInfo { + transaction_receipt: expected_receipts_v0_9[0].clone(), + block_hash: Some(block_hashes[0]), + block_number: 0, + }; + assert_eq!(get_transaction_receipt(&rpc, tx_hashes[0]).unwrap(), res); + + // Block 1 + + // Block 2 + let res = TxnReceiptWithBlockInfo { + transaction_receipt: expected_receipts_v0_9[1].clone(), + block_hash: Some(block_hashes[2]), + block_number: 2, + }; + assert_eq!(get_transaction_receipt(&rpc, tx_hashes[1]).unwrap(), res); + let res = TxnReceiptWithBlockInfo { + transaction_receipt: expected_receipts_v0_9[2].clone(), + block_hash: Some(block_hashes[2]), + block_number: 2, + }; + assert_eq!(get_transaction_receipt(&rpc, tx_hashes[2]).unwrap(), res); + + // Pending + let res = TxnReceiptWithBlockInfo { + transaction_receipt: expected_receipts_v0_9[3].clone(), + block_hash: None, + block_number: 3, + }; + assert_eq!(get_transaction_receipt(&rpc, tx_hashes[3]).unwrap(), res); + } + + #[rstest] + fn test_get_transaction_receipt_not_found(sample_chain_for_block_getters: (SampleChainForBlockGetters, Starknet)) { + let (SampleChainForBlockGetters { .. }, rpc) = sample_chain_for_block_getters; + + let does_not_exist = Felt::from_hex_unchecked("0x7128638126378"); + assert_eq!(get_transaction_receipt(&rpc, does_not_exist), Err(StarknetRpcApiError::TxnHashNotFound)); + } +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_transaction_status.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_transaction_status.rs new file mode 100644 index 0000000000..2d8c5515e6 --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/get_transaction_status.rs @@ -0,0 +1,200 @@ +use crate::errors::{StarknetRpcApiError, StarknetRpcResult}; +use crate::Starknet; +use mp_receipt::ExecutionResult; +use mp_rpc::v0_10_0::{TxnExecutionStatus, TxnFinalityAndExecutionStatus, TxnStatus}; +use starknet_types_core::felt::Felt; + +/// Gets the status of a transaction. +/// +/// Supported statuses are: +/// +/// - [`Received`]: tx has been inserted into the mempool. +/// - [`Candidate`]: tx is a candidate for the pre-confirmed block. +/// - [`PreConfirmed`]: tx is executed in the pre-confirmed block. +/// - [`AcceptedOnL2`]: tx has been saved to the pending block. +/// - [`AcceptedOnL1`]: tx has been finalized on L1. +/// +/// [`Received`]: mp_rpc::v0_10_0::TxnStatus::Received +/// [`Candidate`]: mp_rpc::v0_10_0::TxnStatus::Candidate +/// [`PreConfirmed`]: mp_rpc::v0_10_0::TxnStatus::PreConfirmed +/// [`AcceptedOnL2`]: mp_rpc::v0_10_0::TxnStatus::AcceptedOnL2 +/// [`AcceptedOnL1`]: mp_rpc::v0_10_0::TxnStatus::AcceptedOnL1 +pub async fn get_transaction_status( + starknet: &Starknet, + transaction_hash: Felt, +) -> StarknetRpcResult { + let view = starknet.backend.view_on_latest(); + // TODO: rpc v0.9 Candidate status. + if let Some(res) = view.find_transaction_by_hash(&transaction_hash)? { + let transaction = res.get_transaction()?; + + let execution_status = match transaction.receipt.execution_result() { + ExecutionResult::Reverted { .. } => Some(TxnExecutionStatus::Reverted), + ExecutionResult::Succeeded => Some(TxnExecutionStatus::Succeeded), + }; + + let finality_status = if res.block.is_on_l1() { + TxnStatus::AcceptedOnL1 + } else if res.block.is_confirmed() { + TxnStatus::AcceptedOnL2 + } else { + // FIXME: rpc v0.9 TxnStatus::Preconfirmed + TxnStatus::AcceptedOnL2 + }; + + Ok(TxnFinalityAndExecutionStatus { finality_status, execution_status }) + } else if starknet.add_transaction_provider.received_transaction(transaction_hash).await.is_some_and(|b| b) { + Ok(TxnFinalityAndExecutionStatus { finality_status: TxnStatus::Received, execution_status: None }) + } else { + Err(StarknetRpcApiError::TxnHashNotFound) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const TX_HASH: starknet_types_core::felt::Felt = starknet_types_core::felt::Felt::from_hex_unchecked( + "0x3ccaabf599097d1965e1ef8317b830e76eb681016722c9364ed6e59f3252908", + ); + + #[rstest::fixture] + fn logs() { + let debug = tracing_subscriber::filter::LevelFilter::DEBUG; + let env = tracing_subscriber::EnvFilter::builder().with_default_directive(debug.into()).from_env_lossy(); + let timer = tracing_subscriber::fmt::time::Uptime::default(); + let _ = tracing_subscriber::fmt() + .with_test_writer() + .with_env_filter(env) + .with_file(true) + .with_line_number(true) + .with_target(false) + .with_timer(timer) + .try_init(); + } + + #[rstest::fixture] + fn tx() -> mp_rpc::v0_10_0::BroadcastedInvokeTxn { + mp_rpc::v0_10_0::BroadcastedInvokeTxn::V3(mp_rpc::v0_10_0::InvokeTxnV3 { + calldata: Default::default(), + sender_address: Default::default(), + signature: Default::default(), + nonce: Default::default(), + resource_bounds: mp_rpc::v0_10_0::ResourceBoundsMapping { + l1_gas: mp_rpc::v0_10_0::ResourceBounds { max_amount: 0, max_price_per_unit: 0 }, + l2_gas: mp_rpc::v0_10_0::ResourceBounds { max_amount: 0, max_price_per_unit: 0 }, + l1_data_gas: mp_rpc::v0_10_0::ResourceBounds { max_amount: 0, max_price_per_unit: 0 }, + }, + tip: Default::default(), + paymaster_data: Default::default(), + account_deployment_data: Default::default(), + nonce_data_availability_mode: mp_rpc::v0_10_0::DaMode::L1, + fee_data_availability_mode: mp_rpc::v0_10_0::DaMode::L1, + }) + } + + #[rstest::fixture] + fn tx_with_receipt(tx: mp_rpc::v0_10_0::BroadcastedInvokeTxn) -> mp_block::TransactionWithReceipt { + mp_block::TransactionWithReceipt { + transaction: mp_transactions::Transaction::Invoke(tx.into()), + receipt: mp_receipt::TransactionReceipt::Invoke(mp_receipt::InvokeTransactionReceipt { + transaction_hash: TX_HASH, + execution_result: mp_receipt::ExecutionResult::Succeeded, + ..Default::default() + }), + } + } + + #[rstest::fixture] + fn block(tx_with_receipt: mp_block::TransactionWithReceipt) -> mp_block::FullBlockWithoutCommitments { + mp_block::FullBlockWithoutCommitments { + header: Default::default(), + state_diff: Default::default(), + transactions: vec![tx_with_receipt], + events: Default::default(), + } + } + + #[rstest::fixture] + fn starknet() -> Starknet { + let chain_config = std::sync::Arc::new(mp_chain_config::ChainConfig::madara_test()); + let backend = mc_db::MadaraBackend::open_for_testing(chain_config); + let validation = mc_submit_tx::TransactionValidatorConfig { disable_validation: true, disable_fee: false }; + let mempool = std::sync::Arc::new(mc_mempool::Mempool::new( + std::sync::Arc::clone(&backend), + mc_mempool::MempoolConfig::default(), + )); + let mempool_validator = std::sync::Arc::new(mc_submit_tx::TransactionValidator::new( + mempool, + std::sync::Arc::clone(&backend), + validation, + )); + let context = mp_utils::service::ServiceContext::new_for_testing(); + + Starknet::new(backend, mempool_validator, Default::default(), None, context) + } + + #[tokio::test] + #[rstest::rstest] + async fn get_transaction_status_received(_logs: (), starknet: Starknet, tx: mp_rpc::v0_10_0::BroadcastedInvokeTxn) { + let provider = std::sync::Arc::clone(&starknet.add_transaction_provider); + let result = provider.submit_invoke_transaction(tx).await.expect("Failed to submit invoke transaction"); + let tx_hash = result.transaction_hash; + + let status = get_transaction_status(&starknet, tx_hash).await.expect("Failed to retrieve transaction status"); + + assert_eq!( + status, + TxnFinalityAndExecutionStatus { finality_status: TxnStatus::Received, execution_status: None } + ); + } + + #[tokio::test] + #[rstest::rstest] + async fn get_transaction_status_accepted_on_l2( + _logs: (), + starknet: Starknet, + block: mp_block::FullBlockWithoutCommitments, + ) { + let backend = std::sync::Arc::clone(&starknet.backend); + backend.write_access().add_full_block_with_classes(&block, &[], true).expect("Failed to store pending block"); + + let status = get_transaction_status(&starknet, TX_HASH).await.expect("Failed to retrieve transaction status"); + + assert_eq!( + status, + TxnFinalityAndExecutionStatus { + finality_status: TxnStatus::AcceptedOnL2, + execution_status: Some(mp_rpc::v0_10_0::TxnExecutionStatus::Succeeded) + } + ); + } + + #[tokio::test] + #[rstest::rstest] + async fn get_transaction_status_accepted_on_l1( + _logs: (), + starknet: Starknet, + block: mp_block::FullBlockWithoutCommitments, + ) { + let backend = std::sync::Arc::clone(&starknet.backend); + backend.write_access().add_full_block_with_classes(&block, &[], true).expect("Failed to store pending block"); + backend.set_latest_l1_confirmed(Some(0)).expect("Failed to update last confirmed block"); + + let status = get_transaction_status(&starknet, TX_HASH).await.expect("Failed to retrieve transaction status"); + + assert_eq!( + status, + TxnFinalityAndExecutionStatus { + finality_status: TxnStatus::AcceptedOnL1, + execution_status: Some(mp_rpc::v0_10_0::TxnExecutionStatus::Succeeded) + } + ); + } + + #[tokio::test] + #[rstest::rstest] + async fn get_transaction_status_err_not_found(_logs: (), starknet: Starknet) { + assert_eq!(get_transaction_status(&starknet, TX_HASH).await, Err(StarknetRpcApiError::TxnHashNotFound)); + } +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/mod.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/mod.rs new file mode 100644 index 0000000000..ae5455ea4a --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/read/mod.rs @@ -0,0 +1,170 @@ +use crate::versions::user::v0_8_1::StarknetReadRpcApiV0_8_1Server as V0_8_1Impl; +use crate::versions::user::v0_10_0::StarknetReadRpcApiV0_10_0Server; +use crate::{Starknet, StarknetRpcApiError}; +use jsonrpsee::core::{async_trait, RpcResult}; +use mp_chain_config::RpcVersion; +use mp_convert::Felt; +use mp_rpc::v0_10_0::{ + BlockHashAndNumber, BlockId, BroadcastedTxn, ContractStorageKeysItem, EventFilterWithPageRequest, EventsChunk, + FeeEstimate, FunctionCall, GetStorageProofResult, MaybeDeprecatedContractClass, MaybePreConfirmedBlockWithTxHashes, + MaybePreConfirmedBlockWithTxs, MaybePreConfirmedStateUpdate, MessageFeeEstimate, MsgFromL1, + SimulationFlagForEstimateFee, StarknetGetBlockWithTxsAndReceiptsResult, SyncingStatus, + TxnFinalityAndExecutionStatus, TxnReceiptWithBlockInfo, TxnWithHash, +}; + +pub mod call; +pub mod estimate_fee; +pub mod estimate_message_fee; +pub mod get_block_transaction_count; +pub mod get_block_with_receipts; +pub mod get_block_with_tx_hashes; +pub mod get_block_with_txs; +pub mod get_class; +pub mod get_class_at; +pub mod get_class_hash_at; +pub mod get_events; +pub mod get_nonce; +pub mod get_state_update; +pub mod get_storage_at; +pub mod get_transaction_by_block_id_and_index; +pub mod get_transaction_by_hash; +pub mod get_transaction_receipt; +pub mod get_transaction_status; + +#[async_trait] +impl StarknetReadRpcApiV0_10_0Server for Starknet { + fn spec_version(&self) -> RpcResult { + Ok(RpcVersion::RPC_VERSION_0_9_0.to_string()) + } + + fn block_number(&self) -> RpcResult { + V0_8_1Impl::block_number(self) + } + + fn block_hash_and_number(&self) -> RpcResult { + V0_8_1Impl::block_hash_and_number(self) + } + + fn chain_id(&self) -> RpcResult { + V0_8_1Impl::chain_id(self) + } + + fn syncing(&self) -> RpcResult { + V0_8_1Impl::syncing(self) + } + + async fn call(&self, request: FunctionCall, block_id: BlockId) -> RpcResult> { + Ok(call::call(self, request, block_id).await?) + } + + fn get_block_transaction_count(&self, block_id: BlockId) -> RpcResult { + Ok(get_block_transaction_count::get_block_transaction_count(self, block_id)?) + } + + async fn estimate_fee( + &self, + request: Vec, + simulation_flags: Vec, + block_id: BlockId, + ) -> RpcResult> { + Ok(estimate_fee::estimate_fee(self, request, simulation_flags, block_id).await?) + } + + async fn estimate_message_fee(&self, message: MsgFromL1, block_id: BlockId) -> RpcResult { + Ok(estimate_message_fee::estimate_message_fee(self, message, block_id).await?) + } + + fn get_block_with_receipts(&self, block_id: BlockId) -> RpcResult { + Ok(get_block_with_receipts::get_block_with_receipts(self, block_id)?) + } + + fn get_block_with_tx_hashes(&self, block_id: BlockId) -> RpcResult { + Ok(get_block_with_tx_hashes::get_block_with_tx_hashes(self, block_id)?) + } + + fn get_block_with_txs(&self, block_id: BlockId) -> RpcResult { + Ok(get_block_with_txs::get_block_with_txs(self, block_id)?) + } + + fn get_class_at(&self, block_id: BlockId, contract_address: Felt) -> RpcResult { + Ok(get_class_at::get_class_at(self, block_id, contract_address)?) + } + + fn get_class_hash_at(&self, block_id: BlockId, contract_address: Felt) -> RpcResult { + Ok(get_class_hash_at::get_class_hash_at(self, block_id, contract_address)?) + } + + fn get_class(&self, block_id: BlockId, class_hash: Felt) -> RpcResult { + Ok(get_class::get_class(self, block_id, class_hash)?) + } + + fn get_events(&self, filter: EventFilterWithPageRequest) -> RpcResult { + Ok(get_events::get_events(self, filter)?) + } + + fn get_nonce(&self, block_id: BlockId, contract_address: Felt) -> RpcResult { + Ok(get_nonce::get_nonce(self, block_id, contract_address)?) + } + + fn get_storage_at(&self, contract_address: Felt, key: Felt, block_id: BlockId) -> RpcResult { + Ok(get_storage_at::get_storage_at(self, contract_address, key, block_id)?) + } + + fn get_transaction_by_block_id_and_index(&self, block_id: BlockId, index: u64) -> RpcResult { + Ok(get_transaction_by_block_id_and_index::get_transaction_by_block_id_and_index(self, block_id, index)?) + } + + fn get_transaction_by_hash(&self, transaction_hash: Felt) -> RpcResult { + Ok(get_transaction_by_hash::get_transaction_by_hash(self, transaction_hash)?) + } + + fn get_transaction_receipt(&self, transaction_hash: Felt) -> RpcResult { + Ok(get_transaction_receipt::get_transaction_receipt(self, transaction_hash)?) + } + + async fn get_transaction_status(&self, transaction_hash: Felt) -> RpcResult { + Ok(get_transaction_status::get_transaction_status(self, transaction_hash).await?) + } + + fn get_state_update(&self, block_id: BlockId) -> RpcResult { + Ok(get_state_update::get_state_update(self, block_id)?) + } + fn get_storage_proof( + &self, + block_id: BlockId, + class_hashes: Option>, + contract_addresses: Option>, + contracts_storage_keys: Option>, + ) -> RpcResult { + // support the new block id transparently (preconfirmed blocks are not allowed). + let block_view = self.resolve_view_on(block_id)?; + + // Convert v0_10_0 ContractStorageKeysItem (with Vec) to v0_8_1 (with Vec) + let contracts_storage_keys_v0_8_1 = contracts_storage_keys.map(|keys| { + keys.into_iter() + .map(|item| mp_rpc::v0_8_1::ContractStorageKeysItem { + contract_address: item.contract_address, + storage_keys: item + .storage_keys + .into_iter() + .map(|key| Felt::from_hex(&key).unwrap_or(Felt::ZERO)) + .collect(), + }) + .collect() + }); + + V0_8_1Impl::get_storage_proof( + self, + mp_rpc::v0_8_1::BlockId::Number( + block_view.latest_confirmed_block_n().ok_or(StarknetRpcApiError::NoBlocks)?, + ), + class_hashes, + contract_addresses, + contracts_storage_keys_v0_8_1, + ) + } + + fn get_compiled_casm(&self, class_hash: Felt) -> RpcResult { + V0_8_1Impl::get_compiled_casm(self, class_hash) + } +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/trace/mod.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/trace/mod.rs new file mode 100644 index 0000000000..2ee8072304 --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/trace/mod.rs @@ -0,0 +1,34 @@ +use crate::{versions::user::v0_10_0::StarknetTraceRpcApiV0_10_0Server, Starknet}; +use jsonrpsee::core::{async_trait, RpcResult}; +use mp_rpc::v0_10_0::{ + BlockId, BroadcastedTxn, SimulateTransactionsResult, SimulationFlag, TraceBlockTransactionsResult, + TraceTransactionResult, +}; +use simulate_transactions::simulate_transactions; +use starknet_types_core::felt::Felt; +use trace_block_transactions::trace_block_transactions; +use trace_transaction::trace_transaction; + +pub(crate) mod simulate_transactions; +pub mod trace_block_transactions; +pub(crate) mod trace_transaction; + +#[async_trait] +impl StarknetTraceRpcApiV0_10_0Server for Starknet { + async fn simulate_transactions( + &self, + block_id: BlockId, + transactions: Vec, + simulation_flags: Vec, + ) -> RpcResult> { + Ok(simulate_transactions(self, block_id, transactions, simulation_flags).await?) + } + + async fn trace_block_transactions(&self, block_id: BlockId) -> RpcResult> { + Ok(trace_block_transactions(self, block_id).await?) + } + + async fn trace_transaction(&self, transaction_hash: Felt) -> RpcResult { + Ok(trace_transaction(self, transaction_hash).await?) + } +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/trace/simulate_transactions.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/trace/simulate_transactions.rs new file mode 100644 index 0000000000..9326a8009b --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/trace/simulate_transactions.rs @@ -0,0 +1,69 @@ +use crate::errors::{StarknetRpcApiError, StarknetRpcResult}; +use crate::utils::tx_api_to_blockifier; +use crate::Starknet; +use anyhow::Context; +use blockifier::transaction::account_transaction::ExecutionFlags; +use mc_exec::execution::TxInfo; +use mc_exec::trace::execution_result_to_tx_trace_v0_9; +use mc_exec::{MadaraBlockViewExecutionExt, EXECUTION_UNSUPPORTED_BELOW_VERSION}; +use mp_convert::ToFelt; +use mp_rpc::v0_10_0::{BlockId, BroadcastedTxn, FeeEstimate, PriceUnitFri, SimulateTransactionsResult, SimulationFlag}; +use mp_transactions::{IntoStarknetApiExt, ToBlockifierError}; + +pub async fn simulate_transactions( + starknet: &Starknet, + block_id: BlockId, + transactions: Vec, + simulation_flags: Vec, +) -> StarknetRpcResult> { + 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 charge_fee = !simulation_flags.contains(&SimulationFlag::SkipFeeCharge); + let validate = !simulation_flags.contains(&SimulationFlag::SkipValidate); + + let user_transactions = transactions + .into_iter() + .map(|tx| { + let only_query = tx.is_query(); + let (api_tx, _) = tx + .into_starknet_api(starknet.backend.chain_config().chain_id.to_felt(), exec_context.protocol_version)?; + let execution_flags = ExecutionFlags { only_query, charge_fee, validate, strict_nonce_check: true }; + Ok(tx_api_to_blockifier(api_tx, execution_flags)?) + }) + .collect::, ToBlockifierError>>()?; + + let tips = user_transactions.iter().map(|tx| tx.tip().unwrap_or_default()).collect::>(); + + // 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([], user_transactions)?, exec_context)) + }) + .await?; + + let simulated_transactions = execution_results + .iter() + .zip(tips) + .map(|(result, tip)| { + Ok(SimulateTransactionsResult { + transaction_trace: execution_result_to_tx_trace_v0_9( + result, + exec_context.block_context.versioned_constants(), + ) + .context("Converting execution infos to tx trace")?, + fee_estimation: FeeEstimate { + common: exec_context + .execution_result_to_fee_estimate_v0_9(result, tip) + .context("Converting execution infos to tx trace")?, + unit: PriceUnitFri::Fri, + }, + }) + }) + .collect::, StarknetRpcApiError>>()?; + + Ok(simulated_transactions) +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/trace/trace_block_transactions.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/trace/trace_block_transactions.rs new file mode 100644 index 0000000000..efbb055842 --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/trace/trace_block_transactions.rs @@ -0,0 +1,71 @@ +use crate::errors::{StarknetRpcApiError, StarknetRpcResult}; +use crate::Starknet; +use anyhow::Context; +use mc_db::{MadaraBlockView, MadaraStateView}; +use mc_exec::trace::execution_result_to_tx_trace_v0_9; +use mc_exec::{MadaraBlockViewExecutionExt, EXECUTION_UNSUPPORTED_BELOW_VERSION}; +use mp_block::TransactionWithReceipt; +use mp_convert::ToFelt; +use mp_rpc::v0_10_0::{BlockId, TraceBlockTransactionsResult}; +use mp_transactions::TransactionWithHash; + +pub(super) fn prepare_tx_for_reexecution( + view: &MadaraStateView, + tx: TransactionWithReceipt, +) -> anyhow::Result { + let class = if let Some(tx) = tx.transaction.as_declare() { + Some( + view.get_class_info_and_compiled(tx.class_hash())? + .with_context(|| format!("No class found for class_hash={:#x}", tx.class_hash()))?, + ) + } else { + None + }; + + TransactionWithHash::new(tx.transaction, *tx.receipt.transaction_hash()) + .into_blockifier(class.as_ref()) + .context("Error converting transaction to blockifier format for reexecution") +} + +pub async fn trace_block_transactions_view( + view: &MadaraBlockView, +) -> StarknetRpcResult> { + let mut exec_context = view.new_execution_context_at_block_start()?; + + if exec_context.protocol_version < EXECUTION_UNSUPPORTED_BELOW_VERSION { + return Err(StarknetRpcApiError::unsupported_txn_version()); + } + + let state_view = view.state_view(); + let transactions: Vec<_> = view + .get_executed_transactions(..)? + .into_iter() + .map(|tx| prepare_tx_for_reexecution(&state_view, tx)) + .collect::>()?; + + let (executions_results, exec_context) = mp_utils::spawn_blocking(move || { + Ok::<_, mc_exec::Error>((exec_context.execute_transactions([], transactions)?, exec_context)) + }) + .await?; + + let traces = executions_results + .into_iter() + .map(|result| { + let transaction_hash = result.hash.to_felt(); + let trace_root = + execution_result_to_tx_trace_v0_9(&result, exec_context.block_context.versioned_constants()) + .context("Converting execution infos to tx trace")?; + Ok(TraceBlockTransactionsResult { trace_root, transaction_hash }) + }) + .collect::, StarknetRpcApiError>>()?; + + Ok(traces) +} + +pub async fn trace_block_transactions( + starknet: &Starknet, + block_id: BlockId, +) -> StarknetRpcResult> { + let view = starknet.resolve_block_view(block_id)?; + trace_block_transactions_view(&view).await +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/trace/trace_transaction.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/trace/trace_transaction.rs new file mode 100644 index 0000000000..dc9487128e --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/trace/trace_transaction.rs @@ -0,0 +1,48 @@ +use crate::errors::StarknetRpcResult; +use crate::versions::user::v0_10_0::methods::trace::trace_block_transactions::prepare_tx_for_reexecution; +use crate::{Starknet, StarknetRpcApiError}; +use anyhow::Context; +use mc_exec::trace::execution_result_to_tx_trace_v0_9; +use mc_exec::{MadaraBlockViewExecutionExt, EXECUTION_UNSUPPORTED_BELOW_VERSION}; +use mp_rpc::v0_10_0::TraceTransactionResult; +use starknet_types_core::felt::Felt; + +pub async fn trace_transaction( + starknet: &Starknet, + transaction_hash: Felt, +) -> StarknetRpcResult { + let view = starknet.backend.view_on_latest(); + let res = view.find_transaction_by_hash(&transaction_hash)?.ok_or(StarknetRpcApiError::TxnHashNotFound)?; + let mut exec_context = res.block.new_execution_context_at_block_start()?; + + if exec_context.protocol_version < EXECUTION_UNSUPPORTED_BELOW_VERSION { + return Err(StarknetRpcApiError::unsupported_txn_version()); + } + + let state_view = res.block.state_view(); + // Takes up until but not including the transaction we're interested in. + let previous_transactions: Vec<_> = res + .block + .get_executed_transactions(..res.transaction_index)? + .into_iter() + .map(|tx| prepare_tx_for_reexecution(&state_view, tx)) + .collect::>()?; + + let transaction_to_trace = prepare_tx_for_reexecution(&state_view, res.get_transaction()?)?; + + // Reexecute all transactions before the one we're interested in, and trace the one we're interested in.ƒ + let (mut executions_results, exec_context) = mp_utils::spawn_blocking(move || { + Ok::<_, mc_exec::Error>(( + exec_context.execute_transactions(previous_transactions, [transaction_to_trace])?, + exec_context, + )) + }) + .await?; + + let execution_result = executions_results.pop().context("No execution info returned")?; + + let trace = execution_result_to_tx_trace_v0_9(&execution_result, exec_context.block_context.versioned_constants()) + .context("Converting execution infos to tx trace")?; + + Ok(TraceTransactionResult { trace }) +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/write/mod.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/write/mod.rs new file mode 100644 index 0000000000..e2b06e38c7 --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/write/mod.rs @@ -0,0 +1,56 @@ +use crate::versions::user::v0_8_1::StarknetWriteRpcApiV0_8_1Server as V0_8_1Impl; +use crate::{versions::user::v0_10_0::StarknetWriteRpcApiV0_10_0Server, Starknet}; +use jsonrpsee::core::{async_trait, RpcResult}; +use mp_rpc::v0_10_0::{ + AddInvokeTransactionResult, BroadcastedDeclareTxn, BroadcastedDeployAccountTxn, BroadcastedInvokeTxn, + ClassAndTxnHash, ContractAndTxnHash, +}; + +#[async_trait] +impl StarknetWriteRpcApiV0_10_0Server for Starknet { + /// Submit a new declare transaction to be added to the chain + /// + /// # Arguments + /// + /// * `declare_transaction` - the declare transaction to be added to the chain + /// + /// # Returns + /// + /// * `declare_transaction_result` - the result of the declare transaction + async fn add_declare_transaction(&self, declare_transaction: BroadcastedDeclareTxn) -> RpcResult { + V0_8_1Impl::add_declare_transaction(self, declare_transaction).await + } + + /// Add an Deploy Account Transaction + /// + /// # Arguments + /// + /// * `deploy account transaction` - + /// + /// # Returns + /// + /// * `transaction_hash` - transaction hash corresponding to the invocation + /// * `contract_address` - address of the deployed contract account + async fn add_deploy_account_transaction( + &self, + deploy_account_transaction: BroadcastedDeployAccountTxn, + ) -> RpcResult { + V0_8_1Impl::add_deploy_account_transaction(self, deploy_account_transaction).await + } + + /// Add an Invoke Transaction to invoke a contract function + /// + /// # Arguments + /// + /// * `invoke tx` - + /// + /// # Returns + /// + /// * `transaction_hash` - transaction hash corresponding to the invocation + async fn add_invoke_transaction( + &self, + invoke_transaction: BroadcastedInvokeTxn, + ) -> RpcResult { + V0_8_1Impl::add_invoke_transaction(self, invoke_transaction).await + } +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/ws/lib.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/ws/lib.rs new file mode 100644 index 0000000000..600a8b2d18 --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/ws/lib.rs @@ -0,0 +1,58 @@ +use mp_rpc::v0_10_0::BlockId; +use starknet_types_core::felt::Felt; + +use crate::{versions::user::v0_10_0::StarknetWsRpcApiV0_10_0Server, StarknetRpcApiError}; + +use super::starknet_unsubscribe::*; +// use super::subscribe_events::*; +// use super::subscribe_new_heads::*; +// use super::subscribe_pending_transactions::*; +// use super::subscribe_transaction_status::*; + +#[jsonrpsee::core::async_trait] +// FIXME(subscriptions): Remove this #[allow(unused)] once subscriptions are back. +#[allow(unused)] +impl StarknetWsRpcApiV0_10_0Server for crate::Starknet { + async fn subscribe_new_heads( + &self, + subscription_sink: jsonrpsee::PendingSubscriptionSink, + block: BlockId, + ) -> jsonrpsee::core::SubscriptionResult { + // Ok(subscribe_new_heads(self, subscription_sink, block).await?) + Err(StarknetRpcApiError::UnimplementedMethod.into()) + } + + async fn subscribe_events( + &self, + subscription_sink: jsonrpsee::PendingSubscriptionSink, + from_address: Option, + keys: Option>>, + block: Option, + ) -> jsonrpsee::core::SubscriptionResult { + // Ok(subscribe_events(self, subscription_sink, from_address, keys, block).await?) + Err(StarknetRpcApiError::UnimplementedMethod.into()) + } + + async fn subscribe_transaction_status( + &self, + subscription_sink: jsonrpsee::PendingSubscriptionSink, + transaction_hash: Felt, + ) -> jsonrpsee::core::SubscriptionResult { + // Ok(subscribe_transaction_status(self, subscription_sink, transaction_hash).await?) + Err(StarknetRpcApiError::UnimplementedMethod.into()) + } + + async fn subscribe_pending_transactions( + &self, + subscription_sink: jsonrpsee::PendingSubscriptionSink, + transaction_details: bool, + sender_address: Vec, + ) -> jsonrpsee::core::SubscriptionResult { + // Ok(subscribe_pending_transactions(self, subscription_sink, transaction_details, sender_address).await?) + Err(StarknetRpcApiError::UnimplementedMethod.into()) + } + + async fn starknet_unsubscribe(&self, subscription_id: u64) -> jsonrpsee::core::RpcResult { + Ok(starknet_unsubscribe(self, subscription_id).await?) + } +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/ws/mod.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/ws/mod.rs new file mode 100644 index 0000000000..4678a99f2f --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/ws/mod.rs @@ -0,0 +1,36 @@ +pub mod lib; +pub mod starknet_unsubscribe; +// FIXME(subscriptions): Re-add subscriptions. +// pub mod subscribe_events; +// FIXME(subscriptions): Re-add subscriptions. +// pub mod subscribe_new_heads; +// FIXME(subscriptions): Re-add subscriptions. +// pub mod subscribe_pending_transactions; +// FIXME(subscriptions): Re-add subscriptions. +// pub mod subscribe_transaction_status; + +// FIXME(subscriptions): Remove this #[allow(unused)] once subscriptions are back. +#[allow(unused)] +const BLOCK_PAST_LIMIT: u64 = 1024; +// FIXME(subscriptions): Remove this #[allow(unused)] once subscriptions are back. +#[allow(unused)] +const ADDRESS_FILTER_LIMIT: u64 = 128; + +#[derive(PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)] +pub struct SubscriptionItem { + subscription_id: u64, + result: T, +} + +impl SubscriptionItem { + pub fn new(subscription_id: jsonrpsee::types::SubscriptionId, result: T) -> Self { + let subscription_id = match subscription_id { + jsonrpsee::types::SubscriptionId::Num(id) => id, + jsonrpsee::types::SubscriptionId::Str(_) => { + unreachable!("Jsonrpsee middleware has been configured to use u64 subscription ids") + } + }; + + Self { subscription_id, result } + } +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/ws/starknet_unsubscribe.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/ws/starknet_unsubscribe.rs new file mode 100644 index 0000000000..9e60f817d2 --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/ws/starknet_unsubscribe.rs @@ -0,0 +1,56 @@ +//! # Caution +//! +//! This is a temporary workaround due to limitations in the way in which [jsonrpsee] works. If +//! possible at all, clients should prefer to use the unsubscribe methods defined in [api.rs]. These +//! follow the structure `starknet_unsubscribeMethodName`, so for example +//! `starknet_unsubscribeNewHeads`. +//! +//! Use these if you encounter any strange edge cases such as 500 error codes on unsubscribe. +//! +//! [api.rs]: super::super::super::api + +pub async fn starknet_unsubscribe(starknet: &crate::Starknet, subscription_id: u64) -> crate::StarknetRpcResult { + if starknet.ws_handles.subscription_close(subscription_id).await { + Ok(true) + } else { + Err(crate::StarknetRpcApiError::InvalidSubscriptionId) + } +} + +#[cfg(test)] +mod test { + #[rstest::fixture] + fn logs() { + let debug = tracing_subscriber::filter::LevelFilter::DEBUG; + let env = tracing_subscriber::EnvFilter::builder().with_default_directive(debug.into()).from_env_lossy(); + let _ = tracing_subscriber::fmt().with_test_writer().with_env_filter(env).with_line_number(true).try_init(); + } + + #[rstest::fixture] + fn starknet() -> crate::Starknet { + let chain_config = std::sync::Arc::new(mp_chain_config::ChainConfig::madara_test()); + let backend = mc_db::MadaraBackend::open_for_testing(chain_config); + let validation = mc_submit_tx::TransactionValidatorConfig { disable_validation: true, disable_fee: true }; + let mempool = std::sync::Arc::new(mc_mempool::Mempool::new( + std::sync::Arc::clone(&backend), + mc_mempool::MempoolConfig::default(), + )); + let mempool_validator = std::sync::Arc::new(mc_submit_tx::TransactionValidator::new( + mempool, + std::sync::Arc::clone(&backend), + validation, + )); + let context = mp_utils::service::ServiceContext::new_for_testing(); + + crate::Starknet::new(backend, mempool_validator, Default::default(), None, context) + } + + #[tokio::test] + #[rstest::rstest] + async fn starknet_unsubscribe_err(_logs: (), starknet: crate::Starknet) { + assert_eq!( + super::starknet_unsubscribe(&starknet, 0).await, + Err(crate::StarknetRpcApiError::InvalidSubscriptionId) + ) + } +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/ws/subscribe_events.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/ws/subscribe_events.rs new file mode 100644 index 0000000000..1f42520d68 --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/ws/subscribe_events.rs @@ -0,0 +1,367 @@ +use crate::errors::{ErrorExtWs, StarknetWsApiError}; +use mp_block::{ + event_with_info::{drain_block_events, event_match_filter}, + BlockId, +}; +use mp_rpc::v0_8_1::EmittedEvent; +use starknet_types_core::felt::Felt; + +use super::BLOCK_PAST_LIMIT; + +pub async fn subscribe_events( + starknet: &crate::Starknet, + subscription_sink: jsonrpsee::PendingSubscriptionSink, + from_address: Option, + keys: Option>>, + block_id: Option, +) -> Result<(), StarknetWsApiError> { + let sink = subscription_sink.accept().await.or_internal_server_error("Failed to establish websocket connection")?; + let ctx = starknet.ws_handles.subscription_register(sink.subscription_id()).await; + + let mut rx = starknet.backend.subscribe_events(from_address); + + if let Some(block_id) = block_id { + let latest_block = starknet + .backend + .get_latest_block_n() + .or_internal_server_error("Failed to retrieve latest block")? + .ok_or(StarknetWsApiError::NoBlocks)?; + + let block_n = starknet + .backend + .resolve_block_id(&block_id) + .or_internal_server_error("Failed to resolve block id")? + .ok_or(StarknetWsApiError::BlockNotFound)? + .block_n() + .ok_or(StarknetWsApiError::Pending)?; + + if block_n < latest_block.saturating_sub(BLOCK_PAST_LIMIT) { + return Err(StarknetWsApiError::TooManyBlocksBack); + } + for block_number in block_n..=latest_block { + let block = starknet + .get_block(&BlockId::Number(block_number)) + .or_internal_server_error("Failed to retrieve block")?; + for event in drain_block_events(block) + .filter(|event| event_match_filter(&event.event, from_address.as_ref(), keys.as_deref())) + { + send_event(event, &sink).await?; + } + } + } + + loop { + let event = tokio::select! { + event = rx.recv() => event.or_internal_server_error("Failed to retrieve event")?, + _ = sink.closed() => return Ok(()), + _ = ctx.cancelled() => return Err(crate::errors::StarknetWsApiError::Internal) + }; + + if event_match_filter(&event.event, from_address.as_ref(), keys.as_deref()) { + send_event(event, &sink).await?; + } + } +} + +async fn send_event( + event: mp_block::EventWithInfo, + sink: &jsonrpsee::server::SubscriptionSink, +) -> Result<(), StarknetWsApiError> { + let event = EmittedEvent::from(event); + let item = super::SubscriptionItem::new(sink.subscription_id(), event); + let msg = jsonrpsee::SubscriptionMessage::from_json(&item) + .or_internal_server_error("Failed to create response message")?; + sink.send(msg).await.or_internal_server_error("Failed to respond to websocket request") +} + +#[cfg(test)] +mod test { + use crate::{ + versions::user::v0_8_1::{StarknetWsRpcApiV0_8_1Client, StarknetWsRpcApiV0_8_1Server}, + Starknet, + }; + + use super::*; + use crate::test_utils::rpc_test_setup; + use jsonrpsee::ws_client::WsClientBuilder; + use mp_receipt::{InvokeTransactionReceipt, TransactionReceipt}; + use mp_rpc::v0_8_1::{EmittedEvent, Event, EventContent}; + + /// Generates a transaction receipt with predictable event values for testing purposes. + /// Values are generated using binary patterns for easy verification. + /// + /// # Values Pattern (in binary) + /// For a given base B: + /// - Transaction hash = B << 32 + /// - For each event i: + /// - from_address = (B << 32) | (i << 16) | 1 + /// - keys[j] = (B << 32) | (i << 16) | (2 + j) + /// + /// This means: + /// - Top 32 bits: base value + /// - Next 16 bits: event index + /// - Last 16 bits: value type (1 for address, 2+ for keys) + /// + /// # Arguments + /// * `base` - Base number used as prefix for all values + /// * `num_events` - Number of events to generate + /// * `keys_per_event` - Number of keys per event + fn generate_receipt(base: u64, num_events: usize, keys_per_event: usize) -> TransactionReceipt { + // Transaction hash is just the base shifted + let tx_hash = Felt::from(base << 32); + + let events = (0..num_events) + .map(|event_idx| { + // Base pattern for this event: (base << 32) | (event_idx << 16) + let event_pattern = (base << 32) | ((event_idx as u64) << 16); + + // from_address adds 1 to the pattern + let from_address = Felt::from(event_pattern | 1); + + // Keys add 2+ to the pattern + let keys = + (0..keys_per_event).map(|key_idx| Felt::from(event_pattern | (2 + key_idx as u64))).collect(); + + mp_receipt::Event { from_address, keys, data: vec![] } + }) + .collect(); + + TransactionReceipt::Invoke(InvokeTransactionReceipt { transaction_hash: tx_hash, events, ..Default::default() }) + } + + // Generator function that produces a stream of blocks containing events + // Each block contains two receipts: + // 1. First receipt with 1 event and 1 key + // 2. Second receipt with 2 events and 2 keys + fn block_generator(backend: &mc_db::MadaraBackend) -> impl Iterator> + '_ { + (0..).map(|n| { + let block_info = mp_block::MadaraBlockInfo { + header: mp_block::Header { parent_block_hash: Felt::from(n), block_number: n, ..Default::default() }, + block_hash: Felt::from(n), + tx_hashes: vec![], + }; + + let receipts = vec![generate_receipt(n * 2, 1, 1), generate_receipt(n * 2 + 1, 2, 2)]; + + let block_inner = mp_block::MadaraBlockInner { transactions: vec![], receipts }; + + backend + .store_block( + mp_block::MadaraMaybePendingBlock { + info: mp_block::MadaraMaybePreconfirmedBlockInfo::Confirmed(block_info.clone()), + inner: block_inner.clone(), + }, + mp_state_update::StateDiff::default(), + vec![], + ) + .expect("Storing block"); + + block_inner + .receipts + .into_iter() + .flat_map(|receipt| { + let tx_hash = receipt.transaction_hash(); + receipt.into_events().into_iter().map(move |events| (tx_hash, events)) + }) + .map(|(transaction_hash, event)| EmittedEvent { + event: Event { + from_address: event.from_address, + event_content: EventContent { keys: event.keys, data: event.data }, + }, + block_hash: Some(block_info.block_hash), + block_number: Some(block_info.header.block_number), + transaction_hash, + }) + .collect() + }) + } + + // Test 1: Basic event subscription without any filters + // - Creates 10 blocks with events + // - Verifies that all 30 events are received (3 events per block * 10 blocks) + // - Events should arrive in the same order they were generated + #[tokio::test] + #[rstest::rstest] + async fn subscribe_events_no_filter(rpc_test_setup: (std::sync::Arc, Starknet)) { + let (backend, starknet) = rpc_test_setup; + let server = jsonrpsee::server::Server::builder().build("127.0.0.1:0").await.expect("Starting server"); + let server_url = format!("ws://{}", server.local_addr().expect("Retrieving server local address")); + let _server_handle = server.start(StarknetWsRpcApiV0_8_1Server::into_rpc(starknet)); + let client = WsClientBuilder::default().build(&server_url).await.expect("Building client"); + + let mut generator = block_generator(&backend); + + let mut sub = client.subscribe_events(None, None, None).await.expect("Subscribing to events"); + + let mut nb_events = 0; + for _ in 0..10 { + let events = generator.next().expect("Retrieving block"); + for event in events { + let received = sub.next().await.expect("Subscribing closed").expect("Failed to retrieve event"); + assert_eq!(received.result, event); + nb_events += 1; + } + } + assert_eq!(nb_events, 30); + } + + // Test 2: Event subscription filtered by address + // - Creates blocks and filters events by a specific from_address + // - Only events from the specified address should be received + // - Verifies that at least some events match the filter + #[tokio::test] + #[rstest::rstest] + async fn subscribe_events_filter_address(rpc_test_setup: (std::sync::Arc, Starknet)) { + let (backend, starknet) = rpc_test_setup; + let server = jsonrpsee::server::Server::builder().build("127.0.0.1:0").await.expect("Starting server"); + let server_url = format!("ws://{}", server.local_addr().expect("Retrieving server local address")); + let _server_handle = server.start(StarknetWsRpcApiV0_8_1Server::into_rpc(starknet)); + let client = WsClientBuilder::default().build(&server_url).await.expect("Building client"); + + let mut generator = block_generator(&backend); + + let from_address = Felt::from(0x300000001u64); + let mut sub = client.subscribe_events(Some(from_address), None, None).await.expect("Subscribing to events"); + + let mut nb_events = 0; + + for _ in 0..10 { + let events = generator.next().expect("Retrieving block"); + for event in events { + if event.event.from_address == from_address { + let received = sub.next().await.expect("Subscribing closed").expect("Failed to retrieve event"); + assert_eq!(received.result, event); + nb_events += 1; + } + } + } + assert_eq!(nb_events, 1); + } + + // Test 3: Event subscription filtered by keys + // - Creates blocks and filters events by specific key patterns + // - Only events with matching keys should be received + // - Verifies that exactly two specific events match the filter pattern + #[tokio::test] + #[rstest::rstest] + async fn subscribe_events_filter_keys(rpc_test_setup: (std::sync::Arc, Starknet)) { + let (backend, starknet) = rpc_test_setup; + let server = jsonrpsee::server::Server::builder().build("127.0.0.1:0").await.expect("Starting server"); + let server_url = format!("ws://{}", server.local_addr().expect("Retrieving server local address")); + let _server_handle = server.start(StarknetWsRpcApiV0_8_1Server::into_rpc(starknet)); + let client = WsClientBuilder::default().build(&server_url).await.expect("Building client"); + + let mut generator = block_generator(&backend); + + let keys = vec![ + vec![Felt::from(0x300000002u64), Felt::from(0x500000002u64)], + vec![Felt::from(0x300000003u64), Felt::from(0x500000003u64)], + ]; + + let mut sub = client.subscribe_events(None, Some(keys.clone()), None).await.expect("Subscribing to events"); + + let expected_events = vec![ + EmittedEvent { + event: Event { + from_address: Felt::from(0x300000001u64), + event_content: EventContent { + keys: vec![Felt::from(0x300000002u64), Felt::from(0x300000003u64)], + data: vec![], + }, + }, + block_hash: Some(Felt::from(1u64)), + block_number: Some(1), + transaction_hash: Felt::from(0x300000000u64), + }, + EmittedEvent { + event: Event { + from_address: Felt::from(0x500000001u64), + event_content: EventContent { + keys: vec![Felt::from(0x500000002u64), Felt::from(0x500000003u64)], + data: vec![], + }, + }, + block_hash: Some(Felt::from(2u64)), + block_number: Some(2), + transaction_hash: Felt::from(0x500000000u64), + }, + ]; + + for _ in 0..10 { + let _ = generator.next().expect("Retrieving block"); + } + + for event in expected_events { + let received = sub.next().await.expect("Subscribing closed").expect("Failed to retrieve event"); + assert_eq!(received.result, event); + } + } + + // Test 4: Event subscription starting from a past block + // - Generates initial blocks (0-2) + // - Starts subscription from block 3 + // - Verifies that only events from blocks 3-9 are received + // - Events should arrive in the correct order + #[tokio::test] + #[rstest::rstest] + async fn subscribe_events_past_block(rpc_test_setup: (std::sync::Arc, Starknet)) { + let (backend, starknet) = rpc_test_setup; + let server = jsonrpsee::server::Server::builder().build("127.0.0.1:0").await.expect("Starting server"); + let server_url = format!("ws://{}", server.local_addr().expect("Retrieving server local address")); + let _server_handle = server.start(StarknetWsRpcApiV0_8_1Server::into_rpc(starknet)); + let client = WsClientBuilder::default().build(&server_url).await.expect("Building client"); + + let mut generator = block_generator(&backend); + + // Generate first 3 blocks but ignore their events + for _ in 0..3 { + let _ = generator.next().expect("Retrieving block"); + } + + let mut expected_events = vec![]; + + // Collect events from blocks 3-9 + for _ in 3..10 { + let events = generator.next().expect("Retrieving block"); + for event in events { + expected_events.push(event); + } + } + + let block_id = BlockId::Number(3); + let mut sub = client.subscribe_events(None, None, Some(block_id)).await.expect("Subscribing to events"); + + for event in expected_events { + let received = sub.next().await.expect("Subscribing closed").expect("Failed to retrieve event"); + assert_eq!(received.result, event); + } + } + + #[tokio::test] + #[rstest::rstest] + async fn subscribe_events_unsubscribe(rpc_test_setup: (std::sync::Arc, Starknet)) { + let (backend, starknet) = rpc_test_setup; + let server = jsonrpsee::server::Server::builder().build("127.0.0.1:0").await.expect("Starting server"); + let server_url = format!("ws://{}", server.local_addr().expect("Retrieving server local address")); + let _server_handle = server.start(StarknetWsRpcApiV0_8_1Server::into_rpc(starknet)); + let client = WsClientBuilder::default().build(&server_url).await.expect("Building client"); + + let mut generator = block_generator(&backend); + + let mut sub = client.subscribe_events(None, None, None).await.expect("Subscribing to events"); + + let events = generator.next().expect("Retrieving block"); + let subscription_id = sub.next().await.unwrap().unwrap().subscription_id; + client.starknet_unsubscribe(subscription_id).await.expect("Failed to close subscription"); + + let mut nb_events = 0; + for event in events.into_iter().skip(1) { + let received = sub.next().await.expect("Subscribing closed").expect("Failed to retrieve event"); + assert_eq!(received.result, event); + nb_events += 1; + } + assert_eq!(nb_events, 2); + + assert!(sub.next().await.is_none()); + } +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/ws/subscribe_new_heads.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/ws/subscribe_new_heads.rs new file mode 100644 index 0000000000..e8c6f4bd0a --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/ws/subscribe_new_heads.rs @@ -0,0 +1,388 @@ +use mp_rpc::v0_10_0::{BlockId, BlockTag}; + +use super::BLOCK_PAST_LIMIT; +use crate::errors::{ErrorExtWs, OptionExtWs, StarknetWsApiError}; +use std::sync::Arc; + +pub async fn subscribe_new_heads( + starknet: &crate::Starknet, + subscription_sink: jsonrpsee::PendingSubscriptionSink, + block_id: BlockId, +) -> Result<(), StarknetWsApiError> { + let sink = subscription_sink.accept().await.or_internal_server_error("Failed to establish websocket connection")?; + let ctx = starknet.ws_handles.subscription_register(sink.subscription_id()).await; + + let mut block_n = match block_id { + BlockId::Number(block_n) => { + let err = || format!("Failed to retrieve block info for block {block_n}"); + let block_latest = starknet + .backend + .get_block_n(&BlockId::Tag(BlockTag::Latest)) + .or_else_internal_server_error(err)? + .ok_or(StarknetWsApiError::NoBlocks)?; + + if block_n < block_latest.saturating_sub(BLOCK_PAST_LIMIT) { + return Err(StarknetWsApiError::TooManyBlocksBack); + } + + block_n + } + BlockId::Hash(block_hash) => { + let err = || format!("Failed to retrieve block info at hash {block_hash:#x}"); + let block_latest = starknet + .backend + .get_block_n(&BlockId::Tag(BlockTag::Latest)) + .or_else_internal_server_error(err)? + .ok_or(StarknetWsApiError::NoBlocks)?; + + let block_n = starknet + .backend + .get_block_n(&block_id) + .or_else_internal_server_error(err)? + .ok_or(StarknetWsApiError::BlockNotFound)?; + + if block_n < block_latest.saturating_sub(BLOCK_PAST_LIMIT) { + return Err(StarknetWsApiError::TooManyBlocksBack); + } + + block_n + } + BlockId::Tag(BlockTag::Latest) => starknet + .backend + .get_latest_block_n() + .or_internal_server_error("Failed to retrieve block info for latest block")? + .ok_or(StarknetWsApiError::NoBlocks)?, + BlockId::Tag(BlockTag::Pending) => { + return Err(StarknetWsApiError::Pending); + } + }; + + let mut rx = starknet.backend.subscribe_closed_blocks(); + for n in block_n.. { + if sink.is_closed() { + return Ok(()); + } + + let block_info = match starknet.backend.get_block_info(&BlockId::Number(n)) { + Ok(Some(block_info)) => { + let err = || format!("Failed to retrieve block info for block {n}"); + block_info.into_closed().ok_or_else_internal_server_error(err)? + } + Ok(None) => break, + Err(e) => { + let err = format!("Failed to retrieve block info for block {n}: {e}"); + return Err(StarknetWsApiError::internal_server_error(err)); + } + }; + + send_block_header(&sink, block_info, block_n).await?; + block_n = block_n.saturating_add(1); + } + + // Catching up with the backend + loop { + let block_info = tokio::select! { + block_info = rx.recv() => block_info.or_internal_server_error("Failed to retrieve block info")?, + _ = sink.closed() => return Ok(()), + _ = ctx.cancelled() => return Err(crate::errors::StarknetWsApiError::Internal), + }; + + if block_info.header.block_number == block_n { + break send_block_header(&sink, Arc::unwrap_or_clone(block_info), block_n).await?; + } + } + + // New block headers + loop { + let block_info = tokio::select! { + block_info = rx.recv() => block_info.or_internal_server_error("Failed to retrieve block info")?, + _ = sink.closed() => return Ok(()), + _ = ctx.cancelled() => return Err(crate::errors::StarknetWsApiError::Internal), + }; + + if block_info.header.block_number == block_n + 1 { + send_block_header(&sink, Arc::unwrap_or_clone(block_info), block_n).await?; + } else { + let err = + format!("Received non-sequential block {}, expected {}", block_info.header.block_number, block_n + 1); + return Err(StarknetWsApiError::internal_server_error(err)); + } + block_n = block_n.saturating_add(1); + } +} + +async fn send_block_header( + sink: &jsonrpsee::core::server::SubscriptionSink, + block_info: mp_block::MadaraBlockInfo, + block_n: u64, +) -> Result<(), StarknetWsApiError> { + let header = mp_rpc::v0_8_1::BlockHeader::from(block_info); + let item = super::SubscriptionItem::new(sink.subscription_id(), header); + let msg = jsonrpsee::SubscriptionMessage::from_json(&item) + .or_else_internal_server_error(|| format!("Failed to create response message for block {block_n}"))?; + + sink.send(msg).await.or_internal_server_error("Failed to respond to websocket request")?; + + Ok(()) +} + +#[cfg(test)] +mod test { + use super::*; + + use jsonrpsee::ws_client::WsClientBuilder; + use mp_rpc::v0_8_1::BlockHeader; + use starknet_types_core::felt::Felt; + + use crate::{ + test_utils::rpc_test_setup, + versions::user::v0_8_1::{StarknetWsRpcApiV0_8_1Client, StarknetWsRpcApiV0_8_1Server}, + Starknet, + }; + + fn block_generator(backend: &mc_db::MadaraBackend) -> impl Iterator + '_ { + (0..).map(|n| { + backend + .store_block( + mp_block::MadaraMaybePendingBlock { + info: mp_block::MadaraMaybePreconfirmedBlockInfo::Confirmed(mp_block::MadaraBlockInfo { + header: mp_block::Header { + parent_block_hash: Felt::from(n), + block_number: n, + ..Default::default() + }, + block_hash: Felt::from(n), + tx_hashes: vec![], + }), + inner: mp_block::MadaraBlockInner { transactions: vec![], receipts: vec![] }, + }, + mp_state_update::StateDiff::default(), + vec![], + ) + .expect("Storing block"); + + let block_info = backend + .get_block_info(&BlockId::Number(n)) + .expect("Retrieving block info") + .expect("Retrieving block info") + .into_closed() + .expect("Retrieving block info"); + + BlockHeader::from(block_info) + }) + } + + #[tokio::test] + #[rstest::rstest] + async fn subscribe_new_heads(rpc_test_setup: (std::sync::Arc, Starknet)) { + let (backend, starknet) = rpc_test_setup; + let server = jsonrpsee::server::Server::builder().build("127.0.0.1:0").await.expect("Starting server"); + let server_url = format!("ws://{}", server.local_addr().expect("Retrieving server local address")); + // Server will be stopped once this is dropped + let _server_handle = server.start(StarknetWsRpcApiV0_8_1Server::into_rpc(starknet)); + let client = WsClientBuilder::default().build(&server_url).await.expect("Building client"); + + let mut generator = block_generator(&backend); + let expected = generator.next().expect("Retrieving block from backend"); + + let mut sub = + client.subscribe_new_heads(BlockId::Tag(BlockTag::Latest)).await.expect("starknet_subscribeNewHeads"); + + let next = sub.next().await; + let header = next.expect("Waiting for block header").expect("Waiting for block header").result; + + assert_eq!( + header, + expected, + "actual: {}\nexpect: {}", + serde_json::to_string_pretty(&header).unwrap_or_default(), + serde_json::to_string_pretty(&expected).unwrap_or_default() + ); + } + + #[tokio::test] + #[rstest::rstest] + async fn subscribe_new_heads_many(rpc_test_setup: (std::sync::Arc, Starknet)) { + let (backend, starknet) = rpc_test_setup; + let server = jsonrpsee::server::Server::builder().build("127.0.0.1:0").await.expect("Starting server"); + let server_url = format!("ws://{}", server.local_addr().expect("Retrieving server local address")); + // Server will be stopped once this is dropped + let _server_handle = server.start(StarknetWsRpcApiV0_8_1Server::into_rpc(starknet)); + let client = WsClientBuilder::default().build(&server_url).await.expect("Building client"); + + let generator = block_generator(&backend); + let expected: Vec<_> = generator.take(BLOCK_PAST_LIMIT as usize).collect(); + + let mut sub = client.subscribe_new_heads(BlockId::Number(0)).await.expect("starknet_subscribeNewHeads"); + + for e in expected { + let next = sub.next().await; + let header = next.expect("Waiting for block header").expect("Waiting for block header").result; + + assert_eq!( + header, + e, + "actual: {}\nexpect: {}", + serde_json::to_string_pretty(&header).unwrap_or_default(), + serde_json::to_string_pretty(&e).unwrap_or_default() + ); + } + } + + #[tokio::test] + #[rstest::rstest] + async fn subscribe_new_heads_disconnect(rpc_test_setup: (std::sync::Arc, Starknet)) { + let (backend, starknet) = rpc_test_setup; + let server = jsonrpsee::server::Server::builder().build("127.0.0.1:0").await.expect("Starting server"); + let server_url = format!("ws://{}", server.local_addr().expect("Retrieving server local address")); + // Server will be stopped once this is dropped + let _server_handle = server.start(StarknetWsRpcApiV0_8_1Server::into_rpc(starknet)); + let client = WsClientBuilder::default().build(&server_url).await.expect("Building client"); + + let mut generator = block_generator(&backend); + let expected = generator.next().expect("Retrieving block from backend"); + + let mut sub = client.subscribe_new_heads(BlockId::Number(0)).await.expect("starknet_subscribeNewHeads"); + + let next = sub.next().await; + let header = next.expect("Waiting for block header").expect("Waiting for block header").result; + + assert_eq!( + header, + expected, + "actual: {}\nexpect: {}", + serde_json::to_string_pretty(&header).unwrap_or_default(), + serde_json::to_string_pretty(&expected).unwrap_or_default() + ); + + let next = sub.unsubscribe().await; + assert!(next.is_ok()); + } + + #[tokio::test] + #[rstest::rstest] + async fn subscribe_new_heads_future(rpc_test_setup: (std::sync::Arc, Starknet)) { + let (backend, starknet) = rpc_test_setup; + let server = jsonrpsee::server::Server::builder().build("127.0.0.1:0").await.expect("Starting server"); + let server_url = format!("ws://{}", server.local_addr().expect("Retrieving server local address")); + // Server will be stopped once this is dropped + let _server_handle = server.start(StarknetWsRpcApiV0_8_1Server::into_rpc(starknet)); + let client = WsClientBuilder::default().build(&server_url).await.expect("Building client"); + + let mut generator = block_generator(&backend); + let _block_0 = generator.next().expect("Retrieving block from backend"); + + let mut sub = client.subscribe_new_heads(BlockId::Number(1)).await.expect("starknet_subscribeNewHeads"); + + let block_1 = generator.next().expect("Retrieving block from backend"); + + let next = sub.next().await; + let header = next.expect("Waiting for block header").expect("Waiting for block header").result; + + // Note that `sub` does not yield block 0. This is because it starts + // from block 1, ignoring any block before. This can server to notify + // when a block is ready + assert_eq!( + header, + block_1, + "actual: {}\nexpect: {}", + serde_json::to_string_pretty(&header).unwrap_or_default(), + serde_json::to_string_pretty(&block_1).unwrap_or_default() + ); + } + + #[tokio::test] + #[rstest::rstest] + async fn subscribe_new_heads_unsubscribe(rpc_test_setup: (std::sync::Arc, Starknet)) { + let (backend, starknet) = rpc_test_setup; + let server = jsonrpsee::server::Server::builder().build("127.0.0.1:0").await.expect("Starting server"); + let server_url = format!("ws://{}", server.local_addr().expect("Retrieving server local address")); + // Server will be stopped once this is dropped + let _server_handle = server.start(StarknetWsRpcApiV0_8_1Server::into_rpc(starknet)); + let client = WsClientBuilder::default().build(&server_url).await.expect("Building client"); + + let mut generator = block_generator(&backend); + let _block_0 = generator.next().expect("Retrieving block from backend"); + + let mut sub = client.subscribe_new_heads(BlockId::Number(1)).await.expect("starknet_subscribeNewHeads"); + + let _block_1 = generator.next().expect("Retrieving block from backend"); + + let next = sub.next().await; + let subscription_id = next.unwrap().unwrap().subscription_id; + client.starknet_unsubscribe(subscription_id).await.expect("Failed to close subscription"); + + assert!(sub.next().await.is_none()); + } + + #[tokio::test] + #[rstest::rstest] + async fn subscribe_new_heads_err_too_far_back_block_n( + rpc_test_setup: (std::sync::Arc, Starknet), + ) { + let (backend, starknet) = rpc_test_setup; + let server = jsonrpsee::server::Server::builder().build("127.0.0.1:0").await.expect("Starting server"); + let server_url = format!("ws://{}", server.local_addr().expect("Retrieving server local address")); + // Server will be stopped once this is dropped + let _server_handle = server.start(StarknetWsRpcApiV0_8_1Server::into_rpc(starknet)); + let client = WsClientBuilder::default().build(&server_url).await.expect("Building client"); + + // We generate BLOCK_PAST_LIMIT + 2 because genesis is block 0 + let generator = block_generator(&backend); + let _expected: Vec<_> = generator.take(BLOCK_PAST_LIMIT as usize + 2).collect(); + + let mut sub = client.subscribe_new_heads(BlockId::Number(0)).await.expect("starknet_subscribeNewHeads"); + + // Jsonrsee seems to just close the connection and not return the error + // to the client so this is the best we can do :/ + let next = sub.next().await; + assert!(next.is_none()); + } + + #[tokio::test] + #[rstest::rstest] + async fn subscribe_new_heads_err_too_far_back_block_hash( + rpc_test_setup: (std::sync::Arc, Starknet), + ) { + let (backend, starknet) = rpc_test_setup; + let server = jsonrpsee::server::Server::builder().build("127.0.0.1:0").await.expect("Starting server"); + let server_url = format!("ws://{}", server.local_addr().expect("Retrieving server local address")); + // Server will be stopped once this is dropped + let _server_handle = server.start(StarknetWsRpcApiV0_8_1Server::into_rpc(starknet)); + let client = WsClientBuilder::default().build(&server_url).await.expect("Building client"); + + // We generate BLOCK_PAST_LIMIT + 2 because genesis is block 0 + let generator = block_generator(&backend); + let _expected: Vec<_> = generator.take(BLOCK_PAST_LIMIT as usize + 2).collect(); + + let mut sub = + client.subscribe_new_heads(BlockId::Hash(Felt::from(0))).await.expect("starknet_subscribeNewHeads"); + + // Jsonrsee seems to just close the connection and not return the error + // to the client so this is the best we can do :/ + let next = sub.next().await; + assert!(next.is_none()); + } + + #[tokio::test] + #[rstest::rstest] + async fn subscribe_new_heads_err_pending(rpc_test_setup: (std::sync::Arc, Starknet)) { + let (backend, starknet) = rpc_test_setup; + let server = jsonrpsee::server::Server::builder().build("127.0.0.1:0").await.expect("Starting server"); + let server_url = format!("ws://{}", server.local_addr().expect("Retrieving server local address")); + // Server will be stopped once this is dropped + let _server_handle = server.start(StarknetWsRpcApiV0_8_1Server::into_rpc(starknet)); + let client = WsClientBuilder::default().build(&server_url).await.expect("Building client"); + + let generator = block_generator(&backend); + let _expected: Vec<_> = generator.take(BLOCK_PAST_LIMIT as usize + 2).collect(); + + let mut sub = + client.subscribe_new_heads(BlockId::Tag(BlockTag::Pending)).await.expect("starknet_subscribeNewHeads"); + + // Jsonrsee seems to just close the connection and not return the error + // to the client so this is the best we can do :/ + let next = sub.next().await; + assert!(next.is_none()); + } +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/ws/subscribe_pending_transactions.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/ws/subscribe_pending_transactions.rs new file mode 100644 index 0000000000..0fc457d1f3 --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/ws/subscribe_pending_transactions.rs @@ -0,0 +1,493 @@ +use crate::errors::ErrorExtWs; + +/// Notifies the user of new transactions in the pending block which match one of several +/// `sender_address`, +/// +/// The meaning of `sender_address` depends on the transaction type: +/// +/// - [`Invoke`]: **sender address**. +/// - [`L1Handler`]: **L2 contract address**. +/// - [`Declare`]: **sender address**. +/// - [`Deploy`]: **deployed contract address**. +/// - [`DeployAccount`]: **deployed contract address**. +/// +/// Note that it is possible to call this method on a `sender_address` which has not yet been +/// received by the node and this endpoint will send an update as soon as a tx matching that sender +/// address is received. +/// +/// ## Error handling +/// +/// This subscription will issue a connection refusal with [`TooManyAddressesInFilter`] if more than +/// [`ADDRESS_FILTER_LIMIT`] sender addresses are provided. +/// +/// ## DOS mitigation +/// +/// To avoid a malicious attacker keeping connections open indefinitely on a nonexistent sender +/// address, this endpoint will terminate the connection after a global timeout period. This timeout +/// is reset every time a pending block is encountered which contains at least one matching +/// transaction. Essentially, this means that the connection will remain active for as long as a new +/// pending block with matching transactions is found within [`TIMEOUT`] seconds. +/// +/// [`Invoke`]: mp_transactions::Transaction::Invoke +/// [`L1Handler`]: mp_transactions::Transaction::L1Handler +/// [`Declare`]: mp_transactions::Transaction::Declare +/// [`Deploy`]: mp_transactions::Transaction::Deploy +/// [`DeployAccount`]: mp_transactions::Transaction::DeployAccount +/// [`TooManyAddressesInFilter`]: crate::errors::StarknetWsApiError::TooManyAddressesInFilter +/// [`ADDRESS_FILTER_LIMIT`]: super::ADDRESS_FILTER_LIMIT +pub async fn subscribe_pending_transactions( + starknet: &crate::Starknet, + subscription_sink: jsonrpsee::PendingSubscriptionSink, + transaction_details: bool, + sender_address: Vec, +) -> Result<(), crate::errors::StarknetWsApiError> { + let sink = if sender_address.len() as u64 <= super::ADDRESS_FILTER_LIMIT { + subscription_sink.accept().await.or_internal_server_error("Failed to establish websocket connection")? + } else { + subscription_sink.reject(crate::errors::StarknetWsApiError::TooManyAddressesInFilter).await; + return Ok(()); + }; + + let ctx = starknet.ws_handles.subscription_register(sink.subscription_id()).await; + + let mut channel = starknet.backend.subscribe_pending_txs(); + let sender_address = sender_address.into_iter().collect::>(); + loop { + let tx_receipt = tokio::select! { + tx_receipt = channel.recv() => { + tx_receipt + .or_internal_server_error("SubscribePendingTransactions failed to wait on pending transactions")? + }, + _ = sink.closed() => return Ok(()), + _ = ctx.cancelled() => return Err(crate::errors::StarknetWsApiError::Internal), + }; + + let tx_hash = tx_receipt.receipt.transaction_hash(); + let tx = tx_receipt.transaction; + let tx = match tx { + mp_transactions::Transaction::Invoke(ref inner) if sender_address.contains(inner.sender_address()) => tx, + mp_transactions::Transaction::L1Handler(ref inner) if sender_address.contains(&inner.contract_address) => { + tx + } + mp_transactions::Transaction::Declare(ref inner) if sender_address.contains(inner.sender_address()) => tx, + mp_transactions::Transaction::Deploy(ref inner) + if sender_address.contains(&inner.calculate_contract_address()) => + { + tx + } + mp_transactions::Transaction::DeployAccount(ref inner) + if sender_address.contains(&inner.calculate_contract_address()) => + { + tx + } + _ => continue, + }; + + let tx_info = if transaction_details { + mp_rpc::v0_8_1::PendingTxnInfo::Full(tx.into()) + } else { + mp_rpc::v0_8_1::PendingTxnInfo::Hash(tx_hash) + }; + + let item = super::SubscriptionItem::new(sink.subscription_id(), tx_info); + let msg = jsonrpsee::SubscriptionMessage::from_json(&item).or_else_internal_server_error(|| { + format!("SubscribePendingTransactions failed to create response message at tx {tx_hash:#x}") + })?; + + sink.send(msg).await.or_else_internal_server_error(|| { + format!("SubscribePendingTransactions failed to respond to websocket request at tx {tx_hash:#x}") + })?; + } +} + +#[cfg(test)] +mod test { + use crate::{ + versions::user::v0_8_1::{ + methods::ws::SubscriptionItem, StarknetWsRpcApiV0_8_1Client, StarknetWsRpcApiV0_8_1Server, + }, + Starknet, + }; + + const SERVER_ADDR: &str = "127.0.0.1:0"; + const SENDER_ADDRESS: starknet_types_core::felt::Felt = starknet_types_core::felt::Felt::from_hex_unchecked("feed"); + const CONTRACT_ADDRESS: starknet_types_core::felt::Felt = starknet_types_core::felt::Felt::from_hex_unchecked( + "0x64820103001fcf57dc33ea01733a819529381f2df018c97621e4089f0f0d355", + ); + + #[rstest::fixture] + fn logs() { + let debug = tracing_subscriber::filter::LevelFilter::DEBUG; + let env = tracing_subscriber::EnvFilter::builder().with_default_directive(debug.into()).from_env_lossy(); + let _ = tracing_subscriber::fmt().with_test_writer().with_env_filter(env).with_line_number(true).try_init(); + } + + #[rstest::fixture] + fn starknet() -> Starknet { + let chain_config = std::sync::Arc::new(mp_chain_config::ChainConfig::madara_test()); + let backend = mc_db::MadaraBackend::open_for_testing(chain_config); + let validation = mc_submit_tx::TransactionValidatorConfig { disable_validation: true, disable_fee: true }; + let mempool = std::sync::Arc::new(mc_mempool::Mempool::new( + std::sync::Arc::clone(&backend), + mc_mempool::MempoolConfig::default(), + )); + let mempool_validator = std::sync::Arc::new(mc_submit_tx::TransactionValidator::new( + mempool, + std::sync::Arc::clone(&backend), + validation, + )); + let context = mp_utils::service::ServiceContext::new_for_testing(); + + Starknet::new(backend, mempool_validator, Default::default(), None, context) + } + + #[rstest::fixture] + fn receipt() -> mp_receipt::TransactionReceipt { + static HASH: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); + let ordering = std::sync::atomic::Ordering::AcqRel; + let transaction_hash = HASH.fetch_add(1, ordering).into(); + + mp_receipt::TransactionReceipt::Invoke(mp_receipt::InvokeTransactionReceipt { + transaction_hash, + ..Default::default() + }) + } + + #[rstest::fixture] + fn invoke( + #[default(Default::default())] sender_address: starknet_types_core::felt::Felt, + receipt: mp_receipt::TransactionReceipt, + ) -> mp_block::TransactionWithReceipt { + mp_block::TransactionWithReceipt { + transaction: mp_transactions::Transaction::Invoke(mp_transactions::InvokeTransaction::V0( + mp_transactions::InvokeTransactionV0 { contract_address: sender_address, ..Default::default() }, + )), + receipt, + } + } + + #[rstest::fixture] + fn l1_handler( + #[default(Default::default())] contract_address: starknet_types_core::felt::Felt, + receipt: mp_receipt::TransactionReceipt, + ) -> mp_block::TransactionWithReceipt { + mp_block::TransactionWithReceipt { + transaction: mp_transactions::Transaction::L1Handler(mp_transactions::L1HandlerTransaction { + contract_address, + ..Default::default() + }), + receipt, + } + } + + #[rstest::fixture] + fn declare( + #[default(Default::default())] sender_address: starknet_types_core::felt::Felt, + receipt: mp_receipt::TransactionReceipt, + ) -> mp_block::TransactionWithReceipt { + mp_block::TransactionWithReceipt { + transaction: mp_transactions::Transaction::Declare(mp_transactions::DeclareTransaction::V0( + mp_transactions::DeclareTransactionV0 { sender_address, ..Default::default() }, + )), + receipt, + } + } + + #[rstest::fixture] + fn deploy(receipt: mp_receipt::TransactionReceipt) -> mp_block::TransactionWithReceipt { + mp_block::TransactionWithReceipt { + transaction: mp_transactions::Transaction::Deploy(mp_transactions::DeployTransaction::default()), + receipt, + } + } + + #[rstest::fixture] + fn deploy_account(receipt: mp_receipt::TransactionReceipt) -> mp_block::TransactionWithReceipt { + mp_block::TransactionWithReceipt { + transaction: mp_transactions::Transaction::DeployAccount(mp_transactions::DeployAccountTransaction::V1( + mp_transactions::DeployAccountTransactionV1::default(), + )), + receipt, + } + } + + #[tokio::test] + #[rstest::rstest] + async fn subscribe_pending_transactions_ok_hash( + _logs: (), + starknet: Starknet, + #[from(invoke)] + #[with(SENDER_ADDRESS)] + tx_1: mp_block::TransactionWithReceipt, + #[from(invoke)] + #[with(SENDER_ADDRESS)] + tx_2: mp_block::TransactionWithReceipt, + #[from(invoke)] + #[with(starknet_types_core::felt::Felt::ONE)] + #[allow(unused)] + tx_3: mp_block::TransactionWithReceipt, + ) { + let backend = std::sync::Arc::clone(&starknet.backend); + + let builder = jsonrpsee::server::Server::builder(); + let server = builder.build(SERVER_ADDR).await.expect("Failed to start jsonprsee server"); + let server_url = format!("ws://{}", server.local_addr().expect("Failed to retrieve server local addr")); + let _server_handle = server.start(StarknetWsRpcApiV0_8_1Server::into_rpc(starknet)); + + tracing::debug!(server_url, "Started jsonrpsee server"); + + let builder = jsonrpsee::ws_client::WsClientBuilder::default(); + let client = builder.build(&server_url).await.expect("Failed to start jsonrpsee ws client"); + + tracing::debug!("Started jsonrpsee client"); + + let transaction_details = false; + let mut sub = client + .subscribe_pending_transactions(transaction_details, vec![SENDER_ADDRESS]) + .await + .expect("Failed subscription"); + + backend.on_new_pending_tx(tx_3); + backend.on_new_pending_tx(tx_1.clone()); + backend.on_new_pending_tx(tx_2.clone()); + + assert_matches::assert_matches!( + sub.next().await, Some(Ok(SubscriptionItem { result: hash, .. })) => { + assert_matches::assert_matches!( + hash, mp_rpc::v0_8_1::PendingTxnInfo::Hash(hash) => { + assert_eq!(hash, tx_1.receipt.transaction_hash()); + } + ) + } + ); + + tracing::debug!("Received {:#x}", tx_1.receipt.transaction_hash()); + + assert_matches::assert_matches!( + sub.next().await, Some(Ok(SubscriptionItem { result: hash, .. })) => { + assert_matches::assert_matches!( + hash, mp_rpc::v0_8_1::PendingTxnInfo::Hash(hash) => { + assert_eq!(hash, tx_2.receipt.transaction_hash()); + } + ) + } + ); + + tracing::debug!("Received {:#x}", tx_2.receipt.transaction_hash()); + } + + #[tokio::test] + #[rstest::rstest] + async fn subscribe_pending_transactions_ok_details( + _logs: (), + starknet: Starknet, + #[from(invoke)] + #[with(SENDER_ADDRESS)] + tx_1: mp_block::TransactionWithReceipt, + #[from(invoke)] + #[with(SENDER_ADDRESS)] + tx_2: mp_block::TransactionWithReceipt, + #[from(invoke)] + #[with(starknet_types_core::felt::Felt::ONE)] + #[allow(unused)] + tx_3: mp_block::TransactionWithReceipt, + ) { + let backend = std::sync::Arc::clone(&starknet.backend); + + let builder = jsonrpsee::server::Server::builder(); + let server = builder.build(SERVER_ADDR).await.expect("Failed to start jsonprsee server"); + let server_url = format!("ws://{}", server.local_addr().expect("Failed to retrieve server local addr")); + let _server_handle = server.start(StarknetWsRpcApiV0_8_1Server::into_rpc(starknet)); + + tracing::debug!(server_url, "Started jsonrpsee server"); + + let builder = jsonrpsee::ws_client::WsClientBuilder::default(); + let client = builder.build(&server_url).await.expect("Failed to start jsonrpsee ws client"); + + tracing::debug!("Started jsonrpsee client"); + + let transaction_details = true; + let mut sub = client + .subscribe_pending_transactions(transaction_details, vec![SENDER_ADDRESS]) + .await + .expect("Failed subscription"); + + backend.on_new_pending_tx(tx_3); + backend.on_new_pending_tx(tx_1.clone()); + backend.on_new_pending_tx(tx_2.clone()); + + assert_matches::assert_matches!( + sub.next().await, Some(Ok(SubscriptionItem { result: tx, .. })) => { + assert_matches::assert_matches!( + tx, mp_rpc::v0_8_1::PendingTxnInfo::Full(tx) => { + assert_eq!(tx, tx_1.transaction.into()); + } + ) + } + ); + + tracing::debug!("Received {:#x}", tx_1.receipt.transaction_hash()); + + assert_matches::assert_matches!( + sub.next().await, Some(Ok(SubscriptionItem { result: tx, .. })) => { + assert_matches::assert_matches!( + tx, mp_rpc::v0_8_1::PendingTxnInfo::Full(tx) => { + assert_eq!(tx, tx_2.transaction.into()); + } + ) + } + ); + + tracing::debug!("Received {:#x}", tx_2.receipt.transaction_hash()); + } + + #[tokio::test] + #[rstest::rstest] + #[allow(clippy::too_many_arguments)] + async fn subscribe_pending_transaction_ok_all_types( + _logs: (), + starknet: Starknet, + deploy_account: mp_block::TransactionWithReceipt, + deploy: mp_block::TransactionWithReceipt, + #[with(CONTRACT_ADDRESS)] declare: mp_block::TransactionWithReceipt, + #[with(CONTRACT_ADDRESS)] l1_handler: mp_block::TransactionWithReceipt, + #[with(CONTRACT_ADDRESS)] invoke: mp_block::TransactionWithReceipt, + ) { + let backend = std::sync::Arc::clone(&starknet.backend); + + let builder = jsonrpsee::server::Server::builder(); + let server = builder.build(SERVER_ADDR).await.expect("Failed to start jsonprsee server"); + let server_url = format!("ws://{}", server.local_addr().expect("Failed to retrieve server local addr")); + let _server_handle = server.start(StarknetWsRpcApiV0_8_1Server::into_rpc(starknet)); + + tracing::debug!(server_url, "Started jsonrpsee server"); + + let builder = jsonrpsee::ws_client::WsClientBuilder::default(); + let client = builder.build(&server_url).await.expect("Failed to start jsonrpsee ws client"); + + tracing::debug!("Started jsonrpsee client"); + + let transaction_details = false; + let mut sub = client + .subscribe_pending_transactions(transaction_details, vec![CONTRACT_ADDRESS]) + .await + .expect("Failed subscription"); + + backend.on_new_pending_tx(deploy_account.clone()); + backend.on_new_pending_tx(deploy.clone()); + backend.on_new_pending_tx(declare.clone()); + backend.on_new_pending_tx(l1_handler.clone()); + backend.on_new_pending_tx(invoke.clone()); + + assert_matches::assert_matches!( + sub.next().await, Some(Ok(SubscriptionItem { result: hash, .. })) => { + assert_matches::assert_matches!( + hash, mp_rpc::v0_8_1::PendingTxnInfo::Hash(hash) => { + assert_eq!(hash, deploy_account.receipt.transaction_hash()); + } + ) + } + ); + + tracing::debug!("Received {:#x}", deploy_account.receipt.transaction_hash()); + + assert_matches::assert_matches!( + sub.next().await, Some(Ok(SubscriptionItem { result: hash, .. })) => { + assert_matches::assert_matches!( + hash, mp_rpc::v0_8_1::PendingTxnInfo::Hash(hash) => { + assert_eq!(hash, deploy.receipt.transaction_hash()); + } + ) + } + ); + + tracing::debug!("Received {:#x}", deploy.receipt.transaction_hash()); + + assert_matches::assert_matches!( + sub.next().await, Some(Ok(SubscriptionItem { result: hash, .. })) => { + assert_matches::assert_matches!( + hash, mp_rpc::v0_8_1::PendingTxnInfo::Hash(hash) => { + assert_eq!(hash, declare.receipt.transaction_hash()); + } + ) + } + ); + + tracing::debug!("Received {:#x}", declare.receipt.transaction_hash()); + + assert_matches::assert_matches!( + sub.next().await, Some(Ok(SubscriptionItem { result: hash, .. })) => { + assert_matches::assert_matches!( + hash, mp_rpc::v0_8_1::PendingTxnInfo::Hash(hash) => { + assert_eq!(hash, l1_handler.receipt.transaction_hash()); + } + ) + } + ); + + tracing::debug!("Received {:#x}", l1_handler.receipt.transaction_hash()); + + assert_matches::assert_matches!( + sub.next().await, Some(Ok(SubscriptionItem { result: hash, .. })) => { + assert_matches::assert_matches!( + hash, mp_rpc::v0_8_1::PendingTxnInfo::Hash(hash) => { + assert_eq!(hash, invoke.receipt.transaction_hash()); + } + ) + } + ); + + tracing::debug!("Received {:#x}", invoke.receipt.transaction_hash()); + } + + #[tokio::test] + #[rstest::rstest] + async fn subscribe_pending_transactions_err_too_many_sender_address( + _logs: (), + starknet: Starknet, + #[from(invoke)] + #[with(SENDER_ADDRESS)] + #[allow(unused)] + tx_1: mp_block::TransactionWithReceipt, + #[from(invoke)] + #[with(SENDER_ADDRESS)] + #[allow(unused)] + tx_2: mp_block::TransactionWithReceipt, + #[from(invoke)] + #[with(starknet_types_core::felt::Felt::ONE)] + #[allow(unused)] + tx_3: mp_block::TransactionWithReceipt, + ) { + let backend = std::sync::Arc::clone(&starknet.backend); + + let builder = jsonrpsee::server::Server::builder(); + let server = builder.build(SERVER_ADDR).await.expect("Failed to start jsonprsee server"); + let server_url = format!("ws://{}", server.local_addr().expect("Failed to retrieve server local addr")); + let _server_handle = server.start(StarknetWsRpcApiV0_8_1Server::into_rpc(starknet)); + + tracing::debug!(server_url, "Started jsonrpsee server"); + + let builder = jsonrpsee::ws_client::WsClientBuilder::default(); + let client = builder.build(&server_url).await.expect("Failed to start jsonrpsee ws client"); + + tracing::debug!("Started jsonrpsee client"); + + backend.on_new_pending_tx(tx_3); + backend.on_new_pending_tx(tx_1); + backend.on_new_pending_tx(tx_2); + + let transaction_details = false; + let size = super::super::ADDRESS_FILTER_LIMIT as usize + 1; + let err = client + .subscribe_pending_transactions(transaction_details, vec![SENDER_ADDRESS; size]) + .await + .expect_err("Subscription should fail"); + + assert_matches::assert_matches!( + err, + jsonrpsee::core::client::error::Error::Call(err) => { + assert_eq!(err, crate::errors::StarknetWsApiError::TooManyAddressesInFilter.into()); + } + ); + } +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/ws/subscribe_transaction_status.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/ws/subscribe_transaction_status.rs new file mode 100644 index 0000000000..0f9603d794 --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/methods/ws/subscribe_transaction_status.rs @@ -0,0 +1,833 @@ +use crate::errors::ErrorExtWs; + +/// Notifies the subscriber of updates to a transaction's status. ([specs]) +/// +/// Supported statuses are: +/// +/// - [`Received`]: tx has been inserted into the mempool. +/// - [`AcceptedOnL2`]: tx has been saved to the pending block. +/// - [`AcceptedOnL1`]: tx has been finalized on L1. +/// +/// We do not currently support the **Rejected** transaction status. +/// +/// Note that it is possible to call this method on a transaction which has not yet been received by +/// the node and this endpoint will send an update as soon as the tx is received. +/// +/// ## Returns +/// +/// This subscription will automatically close once a transaction has reached [`AcceptedOnL1`]. +/// +/// [specs]: https://github.com/starkware-libs/starknet-specs/blob/a2d10fc6cbaddbe2d3cf6ace5174dd0a306f4885/api/starknet_ws_api.json#L127C5-L168C7 +/// [`Received`]: mp_rpc::v0_8_1::TxnStatus::Received +/// [`AcceptedOnL2`]: mp_rpc::v0_8_1::TxnStatus::AcceptedOnL2 +/// [`AcceptedOnL1`]: mp_rpc::v0_8_1::TxnStatus::AcceptedOnL1 +pub async fn subscribe_transaction_status( + starknet: &crate::Starknet, + subscription_sink: jsonrpsee::PendingSubscriptionSink, + transaction_hash: mp_convert::Felt, +) -> Result<(), crate::errors::StarknetWsApiError> { + let sink = subscription_sink + .accept() + .await + .or_internal_server_error("SubscribeTransactionStatus failed to establish websocket connection")?; + let ctx = starknet.ws_handles.subscription_register(sink.subscription_id()).await; + + SubscriptionState::new(starknet, &sink, &ctx, transaction_hash).await?.drive().await +} + +/// State-machine-based transactions status discovery. +/// +/// The state machine progresses through a series of legals states and transitions as defined by +/// implementors of the [`StateTransition`] trait. Each state is responsible for checking the status +/// of a single transaction state and moving on to the next state check once this has completed. +#[derive(Default)] +enum SubscriptionState<'a> { + #[default] + None, + WaitReceived(StateTransitionReceived<'a>), + WaitAcceptedOnL2(StateTransitionAcceptedOnL2<'a>), + WaitAcceptedOnL1(StateTransitionAcceptedOnL1<'a>), +} + +impl std::fmt::Debug for SubscriptionState<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::None => write!(f, "None"), + Self::WaitReceived(..) => write!(f, "WaitReceived"), + Self::WaitAcceptedOnL2(..) => write!(f, "WaitAcceptedOnL2"), + Self::WaitAcceptedOnL1(..) => write!(f, "WaitAcceptedOnL1"), + } + } +} + +impl<'a> SubscriptionState<'a> { + /// This function is responsible for initializing the state machine. + /// + /// It does so by determining the initial state of the transaction, which in turn determines in + /// which state the state machine starts. + #[tracing::instrument(skip_all)] + async fn new( + starknet: &'a crate::Starknet, + sink: &'a jsonrpsee::core::server::SubscriptionSink, + ctx: &'a crate::WsSubscriptionGuard, + tx_hash: mp_convert::Felt, + ) -> Result { + let common = StateTransitionCommon { starknet, sink, ctx, tx_hash }; + + // **FOOTGUN!** 💥 + // + // We subscribe to each channel before running status checks against the transaction to + // avoid missing any updates. + let channel_mempool = common.starknet.add_transaction_provider.subscribe_new_transactions().await; + let channel_pending_tx = common.starknet.backend.subscribe_pending_txs(); + let channel_confirmed = common.starknet.backend.subscribe_last_block_on_l1(); + + let block_info = starknet.backend.find_tx_hash_block_info(&tx_hash).or_else_internal_server_error(|| { + format!("SubscribeTransactionStatus failed to retrieve block info for tx {tx_hash:#x}") + })?; + + match block_info { + Some((mp_block::MadaraMaybePreconfirmedBlockInfo::Preconfirmed(block_info), _idx)) => { + // Tx has not yet been accepted on L1 but is included on L2, hence it is marked + // as accepted on L2. We wait for it to be accepted on L1 + let block_number = common + .starknet + .backend + .get_block_n(&mp_rpc::v0_8_1::BlockId::Hash(block_info.header.parent_block_hash)) + .or_else_internal_server_error(|| { + format!("SubscribeTransactionStatus failed to retrieve block number for tx {tx_hash:#x}") + })? + .map(|n| n + 1) + .unwrap_or(0); + tracing::debug!("WaitAcceptedOnL1"); + common.send_txn_status(mp_rpc::v0_8_1::TxnStatus::AcceptedOnL2).await?; + Ok(Self::WaitAcceptedOnL1(StateTransitionAcceptedOnL1 { common, block_number, channel_confirmed })) + } + Some((mp_block::MadaraMaybePreconfirmedBlockInfo::Confirmed(block_info), _idx)) => { + let block_number = block_info.header.block_number; + let confirmed = + common.starknet.backend.get_l1_last_confirmed_block().or_internal_server_error( + "SubscribeTransactionStatus failed to retrieving last confirmed block", + )?; + + // Tx has been accepted on L1, hence it is marked as such. This is the final + // stage of the transaction so the state machine is put in its end state. + if confirmed.is_some_and(|n| block_number <= n) { + tracing::debug!("WaitNone"); + common.send_txn_status(mp_rpc::v0_8_1::TxnStatus::AcceptedOnL1).await?; + Ok(Self::None) + } + // Tx has not yet been accepted on L1 but is included on L2, hence it is marked + // as accepted on L2. We wait for it to be accepted on L1 + else { + tracing::debug!("WaitAcceptedOnL1"); + common.send_txn_status(mp_rpc::v0_8_1::TxnStatus::AcceptedOnL2).await?; + Ok(Self::WaitAcceptedOnL1(StateTransitionAcceptedOnL1 { common, block_number, channel_confirmed })) + } + } + None => { + // Local mempool is the only AddTransactionProvider which allows us to inspect the state + // of received transactions. For other providers (such as when forwarding to a remote + // gateway), we default to assuming that the transaction has been received and wait for + // it to be accepted on L2. + let received = common + .starknet + .add_transaction_provider + .received_transaction(common.tx_hash) + .await + .unwrap_or_default(); + match channel_mempool { + // Tx has not been received yet, we wait for it to be received in the mempool + Some(channel_mempool) if !received => { + tracing::debug!("WaitReceived"); + Ok(Self::WaitReceived(StateTransitionReceived { common, channel_mempool })) + } + // Tx has been received or we are forwarding to a remote gateway (in which case we + // assume the transaction has been received). We wait for it to be accepted on L2. + _ => { + tracing::debug!("WaitAcceptedOnL2"); + common.send_txn_status(mp_rpc::v0_8_1::TxnStatus::Received).await?; + Ok(Self::WaitAcceptedOnL2(StateTransitionAcceptedOnL2 { common, channel_pending_tx })) + } + } + } + } + } + + /// This function is responsible for driving the state machine to completion. It is also + /// responsible for sending status updates back to the client. Status updates are not the + /// responsibility of the [`StateTransition`] implementors and are instead centralized here. + /// + /// ## Legal state transitions + /// + /// ```text + /// + /// ┌────┐ + /// ┌─►│None├─────────────────────────────────────────────────────────┐ + /// │ └────┘ │ + /// │ │ + /// │ │ + /// ┌─────┐ │ ┌────────────┐ ┌────────────────┐ └─►┌───┐ + /// │START├──┼─►│WaitReceived│┬────────────────────────┬──►│WaitAcceptedOnL1│────►│END│ + /// └─────┘ │ └────────────┘│ │ └────────────────┘ └───┘ + /// │ │ ▲ + /// │ └───►┌────────────────┐ │ + /// └────────────────────►│WaitAcceptedOnL2│──┘ + /// └────────────────┘ + /// + /// ``` + #[tracing::instrument()] + async fn drive(&mut self) -> Result<(), crate::errors::StarknetWsApiError> { + loop { + match std::mem::take(self) { + Self::None => return Ok(()), + Self::WaitReceived(state) => { + let s = tokio::select! { + _ = state.common.sink.closed() => break Ok(()), + _ = state.common.ctx.cancelled() => break Err(crate::errors::StarknetWsApiError::Internal), + s = state.transition() => s?, + }; + match s { + TransitionMatrixReceived::WaitAcceptedOnL2(s) => { + s.common.send_txn_status(mp_rpc::v0_8_1::TxnStatus::Received).await?; + *self = Self::WaitAcceptedOnL2(s); + } + TransitionMatrixReceived::WaitAcceptedOnL1(s) => { + s.common.send_txn_status(mp_rpc::v0_8_1::TxnStatus::AcceptedOnL2).await?; + *self = Self::WaitAcceptedOnL1(s); + } + } + } + Self::WaitAcceptedOnL2(state) => { + let s = tokio::select! { + _ = state.common.sink.closed() => break Ok(()), + _ = state.common.ctx.cancelled() => break Err(crate::errors::StarknetWsApiError::Internal), + s = state.transition() => s?, + }; + s.common.send_txn_status(mp_rpc::v0_8_1::TxnStatus::AcceptedOnL2).await?; + *self = Self::WaitAcceptedOnL1(s); + } + Self::WaitAcceptedOnL1(state) => { + let s = tokio::select! { + _ = state.common.sink.closed() => break Ok(()), + _ = state.common.ctx.cancelled() => break Err(crate::errors::StarknetWsApiError::Internal), + s = state.transition() => s?, + }; + s.common.send_txn_status(mp_rpc::v0_8_1::TxnStatus::AcceptedOnL1).await?; + break Ok(()); + } + } + } + } +} + +struct StateTransitionCommon<'a> { + starknet: &'a crate::Starknet, + sink: &'a jsonrpsee::core::server::SubscriptionSink, + ctx: &'a crate::WsSubscriptionGuard, + tx_hash: mp_convert::Felt, +} +struct StateTransitionReceived<'a> { + common: StateTransitionCommon<'a>, + channel_mempool: tokio::sync::broadcast::Receiver, +} +struct StateTransitionAcceptedOnL2<'a> { + common: StateTransitionCommon<'a>, + channel_pending_tx: mc_db::PendingTxsReceiver, +} +struct StateTransitionAcceptedOnL1<'a> { + common: StateTransitionCommon<'a>, + block_number: u64, + channel_confirmed: mc_db::LastBlockOnL1Receiver, +} +struct StateTransitionEnd<'a> { + common: StateTransitionCommon<'a>, +} +enum TransitionMatrixReceived<'a> { + WaitAcceptedOnL2(StateTransitionAcceptedOnL2<'a>), + WaitAcceptedOnL1(StateTransitionAcceptedOnL1<'a>), +} + +impl StateTransitionCommon<'_> { + async fn send_txn_status( + &self, + status: mp_rpc::v0_8_1::TxnStatus, + ) -> Result<(), crate::errors::StarknetWsApiError> { + let txn_status = mp_rpc::v0_8_1::NewTxnStatus { transaction_hash: self.tx_hash, status }; + let item = super::SubscriptionItem::new(self.sink.subscription_id(), txn_status); + let msg = jsonrpsee::SubscriptionMessage::from_json(&item).or_else_internal_server_error(|| { + format!("SubscribeTransactionStatus failed to create response for tx hash {:#x}", self.tx_hash) + })?; + + self.sink + .send(msg) + .await + .or_internal_server_error("SubscribeTransactionStatus failed to respond to websocket request") + } +} + +trait StateTransition: Sized { + type TransitionTo; + + async fn transition(self) -> Result; +} +impl<'a> StateTransition for StateTransitionReceived<'a> { + type TransitionTo = TransitionMatrixReceived<'a>; + + async fn transition(self) -> Result { + let Self { common, mut channel_mempool } = self; + + let channel_confirmed = common.starknet.backend.subscribe_last_block_on_l1(); + let tx_hash = &common.tx_hash; + + loop { + // **FOOTGUN!** 💥 + // + // We delay the pending tx subscription as much as possible so that `WaitAcceptedOnL2` + // will only have to check at most the latest few transactions. If we subscribed to this + // channel at the start of the function, it would be receiving ALL txs from then + // until the transaction was included into the pending block and `WaitAcceptedOnL2` + // would have to check them all! + let channel_pending_tx = common.starknet.backend.subscribe_pending_txs(); + match channel_mempool.recv().await { + Ok(hash) => { + if &hash == tx_hash { + let transition = StateTransitionAcceptedOnL2 { common, channel_pending_tx }; + let transition = Self::TransitionTo::WaitAcceptedOnL2(transition); + break Ok(transition); + } + } + // This happens if the channel lags behind the mempool + Err(_) => { + let block_info = common + .starknet + .backend + .find_tx_hash_block_info(&common.tx_hash) + .or_else_internal_server_error(|| { + format!("SubscribeTransactionStatus failed to retrieve block info for tx {tx_hash:#x}") + })?; + + let Some((mp_block::MadaraMaybePreconfirmedBlockInfo::Confirmed(block_info), _idx)) = block_info else { + continue; + }; + + let block_number = block_info.header.block_number; + let transition = StateTransitionAcceptedOnL1 { common, block_number, channel_confirmed }; + let transition = Self::TransitionTo::WaitAcceptedOnL1(transition); + break Ok(transition); + } + } + } + } +} +impl<'a> StateTransition for StateTransitionAcceptedOnL2<'a> { + type TransitionTo = StateTransitionAcceptedOnL1<'a>; + + async fn transition(self) -> Result { + let Self { common, mut channel_pending_tx } = self; + + let channel_confirmed = common.starknet.backend.subscribe_last_block_on_l1(); + let tx_hash = &common.tx_hash; + + // Step 1: we wait for the tx to be ACCEPTED in the pending block + while let Ok(tx) = channel_pending_tx.recv().await { + if tx.receipt.transaction_hash() == common.tx_hash { + break; + } + } + + // We don't care if this skips an update, we only use this to check against the db on every + // pending block tick + let mut channel_pending_block = common.starknet.backend.subscribe_pending_block(); + + // Step 2: we wait for the tx to be INCLUDED into the pending block. This is necessary since + // the pending tx channel is actually a bit in advance compared to block production and will + // broadcast txs in their batch phase before they are included in the pending block. + let block_number = loop { + let block_info = + common.starknet.backend.find_tx_hash_block_info(&common.tx_hash).or_else_internal_server_error( + || format!("SubscribeTransactionStatus failed to retrieve block info for tx {tx_hash:#x}"), + )?; + match block_info { + Some((mp_block::MadaraMaybePreconfirmedBlockInfo::Preconfirmed(block_info), _idx)) => { + break common + .starknet + .backend + .get_block_n(&mp_rpc::v0_8_1::BlockId::Hash(block_info.header.parent_block_hash)) + .or_else_internal_server_error(|| { + format!("SubscribeTransactionStatus failed to retrieve block number for tx {tx_hash:#x}") + })? + .map(|n| n + 1) + .unwrap_or(0); + } + Some((mp_block::MadaraMaybePreconfirmedBlockInfo::Confirmed(block_info), _idx)) => { + break block_info.header.block_number; + } + None => channel_pending_block + .changed() + .await + .or_internal_server_error("SubscribeTransactionStatus failed to wait for watch channel update")?, + } + }; + + Ok(Self::TransitionTo { common, block_number, channel_confirmed }) + } +} +impl<'a> StateTransition for StateTransitionAcceptedOnL1<'a> { + type TransitionTo = StateTransitionEnd<'a>; + + async fn transition(self) -> Result { + let Self { common, block_number, mut channel_confirmed } = self; + + loop { + let confirmed = channel_confirmed.borrow_and_update().to_owned(); + if confirmed.is_some_and(|n| block_number <= n) { + break Ok(Self::TransitionTo { common }); + } + + // **FOOTGUN!** 💥 + // + // We only wait for L1 confirmed updates AFTER an initial check. This is because all + // previously sent values in a `tokio::sync::watch` channel are marked as seen when we + // first subscribe. If the subscription happens right after an L1 state update, that + // means we would have to wait yet another update before we could read its state, and + // since those are quite infrequent, that can be a lot of time! + channel_confirmed + .changed() + .await + .or_internal_server_error("SubscribeTransactionStatus failed to wait for watch channel update")?; + } + } +} + +#[cfg(test)] +mod test { + use crate::{ + versions::user::v0_8_1::{ + methods::ws::SubscriptionItem, StarknetWsRpcApiV0_8_1Client, StarknetWsRpcApiV0_8_1Server, + }, + Starknet, + }; + + const SERVER_ADDR: &str = "127.0.0.1:0"; + const TX_HASH: starknet_types_core::felt::Felt = starknet_types_core::felt::Felt::from_hex_unchecked( + "0x3ccaabf599097d1965e1ef8317b830e76eb681016722c9364ed6e59f3252908", + ); + + #[rstest::fixture] + fn logs() { + let debug = tracing_subscriber::filter::LevelFilter::DEBUG; + let env = tracing_subscriber::EnvFilter::builder().with_default_directive(debug.into()).from_env_lossy(); + let timer = tracing_subscriber::fmt::time::Uptime::default(); + let _ = tracing_subscriber::fmt() + .with_test_writer() + .with_env_filter(env) + .with_file(true) + .with_line_number(true) + .with_target(false) + .with_timer(timer) + .try_init(); + } + + #[rstest::fixture] + fn tx() -> mp_rpc::v0_8_1::BroadcastedInvokeTxn { + mp_rpc::v0_8_1::BroadcastedInvokeTxn::V0(mp_rpc::v0_8_1::InvokeTxnV0 { + calldata: Default::default(), + contract_address: Default::default(), + entry_point_selector: Default::default(), + max_fee: Default::default(), + signature: Default::default(), + }) + } + + #[rstest::fixture] + fn tx_with_receipt(tx: mp_rpc::v0_8_1::BroadcastedInvokeTxn) -> mp_block::TransactionWithReceipt { + mp_block::TransactionWithReceipt { + transaction: mp_transactions::Transaction::Invoke(tx.into()), + receipt: mp_receipt::TransactionReceipt::Invoke(mp_receipt::InvokeTransactionReceipt { + transaction_hash: TX_HASH, + ..Default::default() + }), + } + } + + #[rstest::fixture] + fn pending(tx_with_receipt: mp_block::TransactionWithReceipt) -> mp_block::PreconfirmedFullBlock { + mp_block::PreconfirmedFullBlock { + header: Default::default(), + state_diff: Default::default(), + transactions: vec![tx_with_receipt], + events: Default::default(), + } + } + + #[rstest::fixture] + fn block(tx_with_receipt: mp_block::TransactionWithReceipt) -> mp_block::MadaraMaybePendingBlock { + mp_block::MadaraMaybePendingBlock { + info: mp_block::MadaraMaybePreconfirmedBlockInfo::Confirmed(mp_block::MadaraBlockInfo { + tx_hashes: vec![TX_HASH], + ..Default::default() + }), + inner: mp_block::MadaraBlockInner { + transactions: vec![tx_with_receipt.transaction], + receipts: vec![tx_with_receipt.receipt], + }, + } + } + + #[rstest::fixture] + fn starknet() -> Starknet { + let chain_config = std::sync::Arc::new(mp_chain_config::ChainConfig::madara_test()); + let backend = mc_db::MadaraBackend::open_for_testing(chain_config); + let validation = mc_submit_tx::TransactionValidatorConfig { disable_validation: true, disable_fee: false }; + let mempool = std::sync::Arc::new(mc_mempool::Mempool::new( + std::sync::Arc::clone(&backend), + mc_mempool::MempoolConfig::default(), + )); + let mempool_validator = std::sync::Arc::new(mc_submit_tx::TransactionValidator::new( + mempool, + std::sync::Arc::clone(&backend), + validation, + )); + let context = mp_utils::service::ServiceContext::new_for_testing(); + + Starknet::new(backend, mempool_validator, Default::default(), None, context) + } + + #[tokio::test] + #[rstest::rstest] + async fn subscribe_transaction_status_received_before( + _logs: (), + starknet: Starknet, + tx: mp_rpc::v0_8_1::BroadcastedInvokeTxn, + ) { + let provider = std::sync::Arc::clone(&starknet.add_transaction_provider); + + let builder = jsonrpsee::server::Server::builder(); + let server = builder.build(SERVER_ADDR).await.expect("Failed to start jsonprsee server"); + let server_url = format!("ws://{}", server.local_addr().expect("Failed to retrieve server local addr")); + let _server_handle = server.start(StarknetWsRpcApiV0_8_1Server::into_rpc(starknet)); + + tracing::debug!(server_url, "Started jsonrpsee server"); + + let builder = jsonrpsee::ws_client::WsClientBuilder::default(); + let client = builder.build(&server_url).await.expect("Failed to start jsonrpsee ws client"); + + tracing::debug!("Started jsonrpsee client"); + + provider.submit_invoke_transaction(tx).await.expect("Failed to submit invoke transaction"); + let mut sub = client.subscribe_transaction_status(TX_HASH).await.expect("Failed subscription"); + + assert_matches::assert_matches!( + sub.next().await, Some(Ok(SubscriptionItem { result: status, .. })) => { + assert_eq!(status, mp_rpc::v0_8_1::NewTxnStatus { + transaction_hash: TX_HASH, + status: mp_rpc::v0_8_1::TxnStatus::Received + }); + } + ); + } + + #[tokio::test] + #[rstest::rstest] + async fn subscribe_transaction_status_received_after( + _logs: (), + starknet: Starknet, + tx: mp_rpc::v0_8_1::BroadcastedInvokeTxn, + ) { + let provider = std::sync::Arc::clone(&starknet.add_transaction_provider); + + let builder = jsonrpsee::server::Server::builder(); + let server = builder.build(SERVER_ADDR).await.expect("Failed to start jsonprsee server"); + let server_url = format!("ws://{}", server.local_addr().expect("Failed to retrieve server local addr")); + let _server_handle = server.start(StarknetWsRpcApiV0_8_1Server::into_rpc(starknet)); + + tracing::debug!(server_url, "Started jsonrpsee server"); + + let builder = jsonrpsee::ws_client::WsClientBuilder::default(); + let client = builder.build(&server_url).await.expect("Failed to start jsonrpsee ws client"); + + tracing::debug!("Started jsonrpsee client"); + + let mut sub = client.subscribe_transaction_status(TX_HASH).await.expect("Failed subscription"); + provider.submit_invoke_transaction(tx).await.expect("Failed to submit invoke transaction"); + + assert_matches::assert_matches!( + sub.next().await, Some(Ok(SubscriptionItem { result: status, .. })) => { + assert_eq!(status, mp_rpc::v0_8_1::NewTxnStatus { + transaction_hash: TX_HASH, + status: mp_rpc::v0_8_1::TxnStatus::Received + }); + } + ); + } + + #[tokio::test] + #[rstest::rstest] + async fn subscribe_transaction_status_accepted_on_l2_before( + _logs: (), + starknet: Starknet, + pending: mp_block::PreconfirmedFullBlock, + ) { + let backend = std::sync::Arc::clone(&starknet.backend); + + let builder = jsonrpsee::server::Server::builder(); + let server = builder.build(SERVER_ADDR).await.expect("Failed to start jsonprsee server"); + let server_url = format!("ws://{}", server.local_addr().expect("Failed to retrieve server local addr")); + let _server_handle = server.start(StarknetWsRpcApiV0_8_1Server::into_rpc(starknet)); + + tracing::debug!(server_url, "Started jsonrpsee server"); + + let builder = jsonrpsee::ws_client::WsClientBuilder::default(); + let client = builder.build(&server_url).await.expect("Failed to start jsonrpsee ws client"); + + tracing::debug!("Started jsonrpsee client"); + + backend.store_pending_block(pending).expect("Failed to store pending block"); + let mut sub = client.subscribe_transaction_status(TX_HASH).await.expect("Failed subscription"); + + assert_matches::assert_matches!( + sub.next().await, Some(Ok(SubscriptionItem { result: status, .. })) => { + assert_eq!(status, mp_rpc::v0_8_1::NewTxnStatus { + transaction_hash: TX_HASH, + status: mp_rpc::v0_8_1::TxnStatus::AcceptedOnL2 + }); + } + ); + } + + #[tokio::test] + #[rstest::rstest] + async fn subscribe_transaction_status_accepted_on_l2_after( + _logs: (), + starknet: Starknet, + tx: mp_rpc::v0_8_1::BroadcastedInvokeTxn, + tx_with_receipt: mp_block::TransactionWithReceipt, + pending: mp_block::PreconfirmedFullBlock, + ) { + let provider = std::sync::Arc::clone(&starknet.add_transaction_provider); + let backend = std::sync::Arc::clone(&starknet.backend); + + let builder = jsonrpsee::server::Server::builder(); + let server = builder.build(SERVER_ADDR).await.expect("Failed to start jsonprsee server"); + let server_url = format!("ws://{}", server.local_addr().expect("Failed to retrieve server local addr")); + let _server_handle = server.start(StarknetWsRpcApiV0_8_1Server::into_rpc(starknet)); + + tracing::debug!(server_url, "Started jsonrpsee server"); + + let builder = jsonrpsee::ws_client::WsClientBuilder::default(); + let client = builder.build(&server_url).await.expect("Failed to start jsonrpsee ws client"); + + tracing::debug!("Started jsonrpsee client"); + + provider.submit_invoke_transaction(tx).await.expect("Failed to submit invoke transaction"); + let mut sub = client.subscribe_transaction_status(TX_HASH).await.expect("Failed subscription"); + + assert_matches::assert_matches!( + sub.next().await, Some(Ok(SubscriptionItem { result: status, .. })) => { + assert_eq!(status, mp_rpc::v0_8_1::NewTxnStatus { + transaction_hash: TX_HASH, + status: mp_rpc::v0_8_1::TxnStatus::Received + }); + } + ); + + backend.on_new_pending_tx(tx_with_receipt); + backend.store_pending_block(pending).expect("Failed to store pending block"); + + assert_matches::assert_matches!( + sub.next().await, Some(Ok(SubscriptionItem { result: status, .. })) => { + assert_eq!(status, mp_rpc::v0_8_1::NewTxnStatus { + transaction_hash: TX_HASH, + status: mp_rpc::v0_8_1::TxnStatus::AcceptedOnL2 + }); + } + ); + } + + #[tokio::test] + #[rstest::rstest] + async fn subscribe_transaction_status_accepted_on_l1_before( + _logs: (), + starknet: Starknet, + block: mp_block::MadaraMaybePendingBlock, + ) { + let backend = std::sync::Arc::clone(&starknet.backend); + + let builder = jsonrpsee::server::Server::builder(); + let server = builder.build(SERVER_ADDR).await.expect("Failed to start jsonprsee server"); + let server_url = format!("ws://{}", server.local_addr().expect("Failed to retrieve server local addr")); + let _server_handle = server.start(StarknetWsRpcApiV0_8_1Server::into_rpc(starknet)); + + tracing::debug!(server_url, "Started jsonrpsee server"); + + let builder = jsonrpsee::ws_client::WsClientBuilder::default(); + let client = builder.build(&server_url).await.expect("Failed to start jsonrpsee ws client"); + + tracing::debug!("Started jsonrpsee client"); + + let state_diff = Default::default(); + let converted_classes = Default::default(); + backend.store_block(block, state_diff, converted_classes).expect("Failed to store block"); + backend.write_last_confirmed_block(0).expect("Failed to update last confirmed block"); + let mut sub = client.subscribe_transaction_status(TX_HASH).await.expect("Failed subscription"); + + assert_matches::assert_matches!( + sub.next().await, Some(Ok(SubscriptionItem { result: status, .. })) => { + assert_eq!(status, mp_rpc::v0_8_1::NewTxnStatus { + transaction_hash: TX_HASH, + status: mp_rpc::v0_8_1::TxnStatus::AcceptedOnL1 + }); + } + ); + } + + #[tokio::test] + #[rstest::rstest] + async fn subscribe_transaction_status_accepted_on_l1_after( + _logs: (), + starknet: Starknet, + block: mp_block::MadaraMaybePendingBlock, + ) { + let backend = std::sync::Arc::clone(&starknet.backend); + + let builder = jsonrpsee::server::Server::builder(); + let server = builder.build(SERVER_ADDR).await.expect("Failed to start jsonprsee server"); + let server_url = format!("ws://{}", server.local_addr().expect("Failed to retrieve server local addr")); + let _server_handle = server.start(StarknetWsRpcApiV0_8_1Server::into_rpc(starknet)); + + tracing::debug!(server_url, "Started jsonrpsee server"); + + let builder = jsonrpsee::ws_client::WsClientBuilder::default(); + let client = builder.build(&server_url).await.expect("Failed to start jsonrpsee ws client"); + + tracing::debug!("Started jsonrpsee client"); + + let state_diff = Default::default(); + let converted_classes = Default::default(); + backend.store_block(block, state_diff, converted_classes).expect("Failed to store block"); + let mut sub = client.subscribe_transaction_status(TX_HASH).await.expect("Failed subscription"); + + assert_matches::assert_matches!( + sub.next().await, Some(Ok(SubscriptionItem { result: status, .. })) => { + assert_eq!(status, mp_rpc::v0_8_1::NewTxnStatus { + transaction_hash: TX_HASH, + status: mp_rpc::v0_8_1::TxnStatus::AcceptedOnL2 + }); + } + ); + + // We assume on our state machine that it is not possible to go from Received directly to + // AcceptedOnL1. This introduces an extra intermediate state to the status check where we + // wait for AcceptedOnL2, which is why this test takes place in two phases. + backend.write_last_confirmed_block(0).expect("Failed to update last confirmed block"); + + assert_matches::assert_matches!( + sub.next().await, Some(Ok(SubscriptionItem { result: status, .. })) => { + assert_eq!(status, mp_rpc::v0_8_1::NewTxnStatus { + transaction_hash: TX_HASH, + status: mp_rpc::v0_8_1::TxnStatus::AcceptedOnL1 + }); + } + ); + } + + #[tokio::test] + #[rstest::rstest] + async fn subscribe_transaction_status_full_flow( + _logs: (), + starknet: Starknet, + tx: mp_rpc::v0_8_1::BroadcastedInvokeTxn, + tx_with_receipt: mp_block::TransactionWithReceipt, + pending: mp_block::PreconfirmedFullBlock, + block: mp_block::MadaraMaybePendingBlock, + ) { + let provider = std::sync::Arc::clone(&starknet.add_transaction_provider); + let backend = std::sync::Arc::clone(&starknet.backend); + + let builder = jsonrpsee::server::Server::builder(); + let server = builder.build(SERVER_ADDR).await.expect("Failed to start jsonprsee server"); + let server_url = format!("ws://{}", server.local_addr().expect("Failed to retrieve server local addr")); + let _server_handle = server.start(StarknetWsRpcApiV0_8_1Server::into_rpc(starknet)); + + tracing::debug!(server_url, "Started jsonrpsee server"); + + let builder = jsonrpsee::ws_client::WsClientBuilder::default(); + let client = builder.build(&server_url).await.expect("Failed to start jsonrpsee ws client"); + + tracing::debug!("Started jsonrpsee client"); + + provider.submit_invoke_transaction(tx).await.expect("Failed to submit invoke transaction"); + let mut sub = client.subscribe_transaction_status(TX_HASH).await.expect("Failed subscription"); + + assert_matches::assert_matches!( + sub.next().await, Some(Ok(SubscriptionItem { result: status, .. })) => { + assert_eq!(status, mp_rpc::v0_8_1::NewTxnStatus { + transaction_hash: TX_HASH, + status: mp_rpc::v0_8_1::TxnStatus::Received + }); + } + ); + + tracing::debug!("Received"); + + backend.on_new_pending_tx(tx_with_receipt); + backend.store_pending_block(pending).expect("Failed to store pending block"); + + assert_matches::assert_matches!( + sub.next().await, Some(Ok(SubscriptionItem { result: status, .. })) => { + assert_eq!(status, mp_rpc::v0_8_1::NewTxnStatus { + transaction_hash: TX_HASH, + status: mp_rpc::v0_8_1::TxnStatus::AcceptedOnL2 + }); + } + ); + + tracing::debug!("AcceptedOnL2"); + + let state_diff = Default::default(); + let converted_classes = Default::default(); + backend.store_block(block, state_diff, converted_classes).expect("Failed to store block"); + backend.write_last_confirmed_block(0).expect("Failed to update last confirmed block"); + + assert_matches::assert_matches!( + sub.next().await, Some(Ok(SubscriptionItem { result: status, .. })) => { + assert_eq!(status, mp_rpc::v0_8_1::NewTxnStatus { + transaction_hash: TX_HASH, + status: mp_rpc::v0_8_1::TxnStatus::AcceptedOnL1 + }); + } + ); + + tracing::debug!("AcceptedOnL1"); + } + + #[tokio::test] + #[rstest::rstest] + async fn subscribe_transaction_status_unsubscribe( + _logs: (), + starknet: Starknet, + tx: mp_rpc::v0_8_1::BroadcastedInvokeTxn, + ) { + let provider = std::sync::Arc::clone(&starknet.add_transaction_provider); + + let builder = jsonrpsee::server::Server::builder(); + let server = builder.build(SERVER_ADDR).await.expect("Failed to start jsonprsee server"); + let server_url = format!("ws://{}", server.local_addr().expect("Failed to retrieve server local addr")); + let _server_handle = server.start(StarknetWsRpcApiV0_8_1Server::into_rpc(starknet)); + + tracing::debug!(server_url, "Started jsonrpsee server"); + + let builder = jsonrpsee::ws_client::WsClientBuilder::default(); + let client = builder.build(&server_url).await.expect("Failed to start jsonrpsee ws client"); + + tracing::debug!("Started jsonrpsee client"); + + provider.submit_invoke_transaction(tx).await.expect("Failed to submit invoke transaction"); + let mut sub = client.subscribe_transaction_status(TX_HASH).await.expect("Failed subscription"); + let subscription_id = sub.next().await.unwrap().unwrap().subscription_id; + + client.starknet_unsubscribe(subscription_id).await.expect("Failed to close subscription"); + assert!(sub.next().await.is_none()); + } +} diff --git a/madara/crates/client/rpc/src/versions/user/v0_10_0/mod.rs b/madara/crates/client/rpc/src/versions/user/v0_10_0/mod.rs new file mode 100644 index 0000000000..128b1a3835 --- /dev/null +++ b/madara/crates/client/rpc/src/versions/user/v0_10_0/mod.rs @@ -0,0 +1,215 @@ +use jsonrpsee::core::RpcResult; +use m_proc_macros::versioned_rpc; +use mp_rpc::v0_10_0::{ + AddInvokeTransactionResult, BlockHashAndNumber, BlockId, BroadcastedDeclareTxn, BroadcastedDeployAccountTxn, + BroadcastedInvokeTxn, BroadcastedTxn, ClassAndTxnHash, ContractAndTxnHash, ContractStorageKeysItem, + EventFilterWithPageRequest, EventsChunk, FeeEstimate, FunctionCall, GetStorageProofResult, + MaybeDeprecatedContractClass, MaybePreConfirmedBlockWithTxHashes, MaybePreConfirmedBlockWithTxs, + MaybePreConfirmedStateUpdate, MessageFeeEstimate, MsgFromL1, SimulateTransactionsResult, SimulationFlag, + SimulationFlagForEstimateFee, StarknetGetBlockWithTxsAndReceiptsResult, SyncingStatus, + TraceBlockTransactionsResult, TraceTransactionResult, TxnFinalityAndExecutionStatus, TxnReceiptWithBlockInfo, + TxnWithHash, +}; +use starknet_types_core::felt::Felt; + +pub mod methods; + +#[versioned_rpc("V0_10_0", "starknet")] +pub trait StarknetReadRpcApi { + #[method(name = "specVersion")] + /// Get the Version of the StarkNet JSON-RPC Specification Being Used + fn spec_version(&self) -> RpcResult; + + /// Get the most recent accepted block number + #[method(name = "blockNumber")] + fn block_number(&self) -> RpcResult; + + // Get the most recent accepted block hash and number + #[method(name = "blockHashAndNumber")] + fn block_hash_and_number(&self) -> RpcResult; + + /// Get an object about the sync status, or false if the node is not syncing + #[method(name = "syncing")] + fn syncing(&self) -> RpcResult; + + /// Get the chain id + #[method(name = "chainId")] + fn chain_id(&self) -> RpcResult; + + /// Call a contract function at a given block id + #[method(name = "call")] + async fn call(&self, request: FunctionCall, block_id: BlockId) -> RpcResult>; + + /// Get the number of transactions in a block given a block id + #[method(name = "getBlockTransactionCount")] + fn get_block_transaction_count(&self, block_id: BlockId) -> RpcResult; + + /// Estimate the fee associated with transaction + #[method(name = "estimateFee")] + async fn estimate_fee( + &self, + request: Vec, + simulation_flags: Vec, + block_id: BlockId, + ) -> RpcResult>; + + /// Estimate the L2 fee of a message sent on L1 + #[method(name = "estimateMessageFee")] + async fn estimate_message_fee(&self, message: MsgFromL1, block_id: BlockId) -> RpcResult; + + /// Get block information with full transactions and receipts given the block id + #[method(name = "getBlockWithReceipts")] + fn get_block_with_receipts(&self, block_id: BlockId) -> RpcResult; + + /// Get block information with transaction hashes given the block id + #[method(name = "getBlockWithTxHashes")] + fn get_block_with_tx_hashes(&self, block_id: BlockId) -> RpcResult; + + /// Get block information with full transactions given the block id + #[method(name = "getBlockWithTxs")] + fn get_block_with_txs(&self, block_id: BlockId) -> RpcResult; + + /// Get the contract class at a given contract address for a given block id + #[method(name = "getClassAt")] + fn get_class_at(&self, block_id: BlockId, contract_address: Felt) -> RpcResult; + + /// Get the contract class hash in the given block for the contract deployed at the given + /// address + #[method(name = "getClassHashAt")] + fn get_class_hash_at(&self, block_id: BlockId, contract_address: Felt) -> RpcResult; + + /// Get the contract class definition in the given block associated with the given hash + #[method(name = "getClass")] + fn get_class(&self, block_id: BlockId, class_hash: Felt) -> RpcResult; + + /// Returns all events matching the given filter + #[method(name = "getEvents")] + fn get_events(&self, filter: EventFilterWithPageRequest) -> RpcResult; + + /// Get the nonce associated with the given address at the given block + #[method(name = "getNonce")] + fn get_nonce(&self, block_id: BlockId, contract_address: Felt) -> RpcResult; + + /// Get the value of the storage at the given address and key, at the given block id + #[method(name = "getStorageAt")] + fn get_storage_at(&self, contract_address: Felt, key: Felt, block_id: BlockId) -> RpcResult; + + /// Get the details of a transaction by a given block id and index + #[method(name = "getTransactionByBlockIdAndIndex")] + fn get_transaction_by_block_id_and_index(&self, block_id: BlockId, index: u64) -> RpcResult; + + /// Returns the information about a transaction by transaction hash. + #[method(name = "getTransactionByHash")] + fn get_transaction_by_hash(&self, transaction_hash: Felt) -> RpcResult; + + /// Returns the receipt of a transaction by transaction hash. + #[method(name = "getTransactionReceipt")] + fn get_transaction_receipt(&self, transaction_hash: Felt) -> RpcResult; + + /// Gets the Transaction Status, Including Mempool Status and Execution Details + #[method(name = "getTransactionStatus")] + async fn get_transaction_status(&self, transaction_hash: Felt) -> RpcResult; + + /// Get the information about the result of executing the requested block + #[method(name = "getStateUpdate")] + fn get_state_update(&self, block_id: BlockId) -> RpcResult; + + #[method(name = "getStorageProof")] + fn get_storage_proof( + &self, + block_id: BlockId, + class_hashes: Option>, + contract_addresses: Option>, + contracts_storage_keys: Option>, + ) -> RpcResult; + + #[method(name = "getCompiledCasm")] + fn get_compiled_casm(&self, class_hash: Felt) -> RpcResult; +} + +type SubscriptionItemPendingTxs = methods::ws::SubscriptionItem; +type SubscriptionItemEvents = methods::ws::SubscriptionItem; +type SubscriptionItemNewHeads = methods::ws::SubscriptionItem; +type SubscriptionItemTransactionStatus = methods::ws::SubscriptionItem; + +#[versioned_rpc("V0_10_0", "starknet")] +pub trait StarknetWsRpcApi { + #[subscription(name = "subscribeNewHeads", unsubscribe = "unsubscribeNewHeads", item = SubscriptionItemNewHeads, param_kind = map)] + async fn subscribe_new_heads(&self, block: BlockId) -> jsonrpsee::core::SubscriptionResult; + + #[subscription( + name = "subscribeEvents", + unsubscribe = "unsubscribeEvents", + item = SubscriptionItemEvents, + param_kind = map + )] + async fn subscribe_events( + &self, + from_address: Option, + keys: Option>>, + block: Option, + ) -> jsonrpsee::core::SubscriptionResult; + + #[subscription( + name = "subscribeTransactionStatus", + unsubscribe = "unsubscribeTransactionStatus", + item = SubscriptionItemTransactionStatus, + param_kind = map + )] + async fn subscribe_transaction_status(&self, transaction_hash: Felt) -> jsonrpsee::core::SubscriptionResult; + + #[subscription( + name = "subscribePendingTransactions", + unsubscribe = "unsubscribePendingTransactions", + item = SubscriptionItemPendingTxs, + param_kind = map + )] + async fn subscribe_pending_transactions( + &self, + transaction_details: bool, + sender_address: Vec, + ) -> jsonrpsee::core::SubscriptionResult; + #[method(name = "unsubscribe")] + async fn starknet_unsubscribe(&self, subscription_id: u64) -> RpcResult; +} + +#[versioned_rpc("V0_10_0", "starknet")] +pub trait StarknetWriteRpcApi { + /// Submit a new transaction to be added to the chain + #[method(name = "addInvokeTransaction")] + async fn add_invoke_transaction( + &self, + invoke_transaction: BroadcastedInvokeTxn, + ) -> RpcResult; + + /// Submit a new deploy account transaction + #[method(name = "addDeployAccountTransaction")] + async fn add_deploy_account_transaction( + &self, + deploy_account_transaction: BroadcastedDeployAccountTxn, + ) -> RpcResult; + + /// Submit a new class declaration transaction + #[method(name = "addDeclareTransaction")] + async fn add_declare_transaction(&self, declare_transaction: BroadcastedDeclareTxn) -> RpcResult; +} + +#[versioned_rpc("V0_10_0", "starknet")] +pub trait StarknetTraceRpcApi { + /// Returns the execution trace of a transaction by simulating it in the runtime. + #[method(name = "simulateTransactions")] + async fn simulate_transactions( + &self, + block_id: BlockId, + transactions: Vec, + simulation_flags: Vec, + ) -> RpcResult>; + + #[method(name = "traceBlockTransactions")] + /// Returns the execution traces of all transactions included in the given block + async fn trace_block_transactions(&self, block_id: BlockId) -> RpcResult>; + + #[method(name = "traceTransaction")] + /// Returns the execution trace of a transaction + async fn trace_transaction(&self, transaction_hash: Felt) -> RpcResult; +} diff --git a/madara/crates/client/sync/src/sync_utils.rs b/madara/crates/client/sync/src/sync_utils.rs index c06005cea5..bb2fd4f30b 100644 --- a/madara/crates/client/sync/src/sync_utils.rs +++ b/madara/crates/client/sync/src/sync_utils.rs @@ -55,6 +55,7 @@ pub async fn compress_state_diff( old_declared_contracts: raw_state_diff.old_declared_contracts, nonces: raw_state_diff.nonces, replaced_classes, + migrated_compiled_classes: vec![], // TODO(prakhar,22/11/2025): Update this }; compressed_diff.sort(); @@ -169,6 +170,7 @@ impl StateDiffMap { old_declared_contracts: deprecated_declared_classes, nonces, replaced_classes, + migrated_compiled_classes: vec![], // TODO(prakhar,22/11/2025): Update this } } } diff --git a/madara/crates/primitives/block/src/event_with_info.rs b/madara/crates/primitives/block/src/event_with_info.rs index 8adc79fab9..8e331f3e8a 100644 --- a/madara/crates/primitives/block/src/event_with_info.rs +++ b/madara/crates/primitives/block/src/event_with_info.rs @@ -40,6 +40,20 @@ impl From for mp_rpc::v0_7_1::EmittedEvent { } } +impl From for mp_rpc::v0_10_0::EmittedEvent { + fn from(event_with_info: EventWithInfo) -> Self { + mp_rpc::v0_10_0::EmittedEvent { + event: event_with_info.event.into(), + block_hash: event_with_info.block_hash, + // v0_10_0 expects None when the event is in the pending block. + block_number: if event_with_info.in_preconfirmed { None } else { Some(event_with_info.block_number) }, + transaction_hash: event_with_info.transaction_hash, + transaction_index: event_with_info.transaction_index, + event_index: event_with_info.event_index_in_block, + } + } +} + /// Filters events based on the provided address and keys. /// /// This function checks if an event matches the given address and keys. diff --git a/madara/crates/primitives/chain_config/src/rpc_version.rs b/madara/crates/primitives/chain_config/src/rpc_version.rs index e2da7ba935..d204d94971 100644 --- a/madara/crates/primitives/chain_config/src/rpc_version.rs +++ b/madara/crates/primitives/chain_config/src/rpc_version.rs @@ -1,10 +1,11 @@ use std::hash::Hash; use std::str::FromStr; -const SUPPORTED_RPC_VERSIONS: [RpcVersion; 4] = [ +const SUPPORTED_RPC_VERSIONS: [RpcVersion; 5] = [ RpcVersion::RPC_VERSION_0_7_1, RpcVersion::RPC_VERSION_0_8_1, RpcVersion::RPC_VERSION_0_9_0, + RpcVersion::RPC_VERSION_0_10_0, RpcVersion::RPC_VERSION_ADMIN_0_1_0, ]; @@ -86,7 +87,8 @@ impl RpcVersion { pub const RPC_VERSION_0_7_1: RpcVersion = RpcVersion([0, 7, 1]); pub const RPC_VERSION_0_8_1: RpcVersion = RpcVersion([0, 8, 1]); pub const RPC_VERSION_0_9_0: RpcVersion = RpcVersion([0, 9, 0]); - pub const RPC_VERSION_LATEST: RpcVersion = Self::RPC_VERSION_0_9_0; + pub const RPC_VERSION_0_10_0: RpcVersion = RpcVersion([0, 10, 0]); + pub const RPC_VERSION_LATEST: RpcVersion = Self::RPC_VERSION_0_10_0; pub const RPC_VERSION_ADMIN_0_1_0: RpcVersion = RpcVersion([0, 1, 0]); pub const RPC_VERSION_LATEST_ADMIN: RpcVersion = Self::RPC_VERSION_ADMIN_0_1_0; diff --git a/madara/crates/primitives/gateway/src/state_update.rs b/madara/crates/primitives/gateway/src/state_update.rs index ea13040e46..01698ce7a1 100644 --- a/madara/crates/primitives/gateway/src/state_update.rs +++ b/madara/crates/primitives/gateway/src/state_update.rs @@ -102,6 +102,7 @@ impl From for mp_state_update::StateDiff { .into_iter() .map(|(contract_address, nonce)| mp_state_update::NonceUpdate { contract_address, nonce }) .collect(), + migrated_compiled_classes: vec![], // TODO(prakhar,22/11/2025): Add migrated compiled classes here } } } diff --git a/madara/crates/primitives/rpc/src/lib.rs b/madara/crates/primitives/rpc/src/lib.rs index ab5e204a97..4a22126426 100644 --- a/madara/crates/primitives/rpc/src/lib.rs +++ b/madara/crates/primitives/rpc/src/lib.rs @@ -4,3 +4,4 @@ pub mod admin; pub mod v0_7_1; pub mod v0_8_1; pub mod v0_9_0; +pub mod v0_10_0; diff --git a/madara/crates/primitives/rpc/src/v0_10_0/mod.rs b/madara/crates/primitives/rpc/src/v0_10_0/mod.rs new file mode 100644 index 0000000000..fac9993b35 --- /dev/null +++ b/madara/crates/primitives/rpc/src/v0_10_0/mod.rs @@ -0,0 +1,132 @@ +//! v0.10.0 of the API. +mod starknet_api_openrpc; +mod starknet_trace_api_openrpc; +mod starknet_ws_api; + +pub use self::starknet_api_openrpc::*; +pub use self::starknet_trace_api_openrpc::*; +pub use self::starknet_ws_api::*; + +#[derive(Debug, Clone, Eq, Hash, PartialEq)] +pub enum BlockId { + /// The tag of the block. + Tag(BlockTag), + /// The hash of the block. + Hash(BlockHash), + /// The height of the block. + Number(BlockNumber), +} + +impl serde::Serialize for BlockId { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + BlockId::Tag(tag) => tag.serialize(serializer), + BlockId::Hash(block_hash) => BlockHashHelper { block_hash: *block_hash }.serialize(serializer), + BlockId::Number(block_number) => BlockNumberHelper { block_number: *block_number }.serialize(serializer), + } + } +} + +impl<'de> serde::Deserialize<'de> for BlockId { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::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)), + } + } +} + +#[derive(serde::Deserialize)] +#[serde(untagged)] +enum BlockIdHelper { + Tag(BlockTag), + Hash(BlockHashHelper), + Number(BlockNumberHelper), +} + +#[test] +fn block_id_from_hash() { + pub use starknet_types_core::felt::Felt; + + let s = "{\"block_hash\":\"0x123\"}"; + let block_id: BlockId = serde_json::from_str(s).unwrap(); + assert_eq!(block_id, BlockId::Hash(Felt::from_hex("0x123").unwrap())); +} + +#[test] +fn block_id_from_number() { + let s = "{\"block_number\":123}"; + let block_id: BlockId = serde_json::from_str(s).unwrap(); + assert_eq!(block_id, BlockId::Number(123)); +} + +#[test] +fn block_id_from_latest() { + let s = "\"latest\""; + let block_id: BlockId = serde_json::from_str(s).unwrap(); + assert_eq!(block_id, BlockId::Tag(BlockTag::Latest)); +} + +#[test] +fn block_id_from_pre_confirmed() { + let s = "\"pre_confirmed\""; + let block_id: BlockId = serde_json::from_str(s).unwrap(); + assert_eq!(block_id, BlockId::Tag(BlockTag::PreConfirmed)); +} + +#[test] +fn block_id_from_l1_accepted() { + let s = "\"l1_accepted\""; + let block_id: BlockId = serde_json::from_str(s).unwrap(); + assert_eq!(block_id, BlockId::Tag(BlockTag::L1Accepted)); +} + +#[cfg(test)] +#[test] +fn block_id_to_hash() { + pub use starknet_types_core::felt::Felt; + + let block_id = BlockId::Hash(Felt::from_hex("0x123").unwrap()); + let s = serde_json::to_string(&block_id).unwrap(); + assert_eq!(s, "{\"block_hash\":\"0x123\"}"); +} + +#[cfg(test)] +#[test] +fn block_id_to_number() { + let block_id = BlockId::Number(123); + let s = serde_json::to_string(&block_id).unwrap(); + assert_eq!(s, "{\"block_number\":123}"); +} + +#[cfg(test)] +#[test] +fn block_id_to_latest() { + let block_id = BlockId::Tag(BlockTag::Latest); + let s = serde_json::to_string(&block_id).unwrap(); + assert_eq!(s, "\"latest\""); +} + +#[cfg(test)] +#[test] +fn block_id_to_pre_confirmed() { + let block_id = BlockId::Tag(BlockTag::PreConfirmed); + let s = serde_json::to_string(&block_id).unwrap(); + assert_eq!(s, "\"pre_confirmed\""); +} + +#[cfg(test)] +#[test] +fn block_id_to_l1_accepted() { + let block_id = BlockId::Tag(BlockTag::L1Accepted); + let s = serde_json::to_string(&block_id).unwrap(); + assert_eq!(s, "\"l1_accepted\""); +} diff --git a/madara/crates/primitives/rpc/src/v0_10_0/starknet_api_openrpc.rs b/madara/crates/primitives/rpc/src/v0_10_0/starknet_api_openrpc.rs new file mode 100644 index 0000000000..af4b99b3df --- /dev/null +++ b/madara/crates/primitives/rpc/src/v0_10_0/starknet_api_openrpc.rs @@ -0,0 +1,108 @@ +pub use crate::v0_9_0::{ + AddDeclareTransactionParams, AddDeployAccountTransactionParams, AddInvokeTransactionParams, + AddInvokeTransactionResult, Address, BlockHash, BlockHashAndNumber, BlockHashAndNumberParams, BlockHashHelper, + BlockHeader, BlockNumber, BlockNumberHelper, BlockNumberParams, BlockStatus, BlockTag, BlockWithReceipts, + BlockWithTxHashes, BlockWithTxs, BroadcastedDeclareTxn, BroadcastedDeclareTxnV1, BroadcastedDeclareTxnV2, + BroadcastedDeclareTxnV3, BroadcastedDeployAccountTxn, BroadcastedInvokeTxn, BroadcastedTxn, CallParams, + ChainId, ChainIdParams, ClassAndTxnHash, CommonReceiptProperties, ContractAbi, ContractAbiEntry, + ContractAndTxnHash, ContractClass, ContractLeavesDataItem, ContractStorageDiffItem, + ContractsProof, DaMode, DataAvailability, DeclareTxn, DeclareTxnReceipt, DeclareTxnV0, DeclareTxnV1, + DeclareTxnV2, DeclareTxnV3, DeployAccountTxn, DeployAccountTxnReceipt, DeployAccountTxnV1, DeployAccountTxnV3, + DeployTxn, DeployTxnReceipt, DeployedContractItem, DeprecatedCairoEntryPoint, DeprecatedContractClass, + DeprecatedEntryPointsByType, EntryPointsByType, EstimateFeeParams, EstimateMessageFeeParams, EthAddress, + Event, EventAbiEntry, EventAbiType, EventContent, EventFilterWithPageRequest, EventsChunk, ExecutionResources, + ExecutionStatus, FeeEstimate, FeeEstimateCommon, FeePayment, FunctionAbiEntry, FunctionAbiType, FunctionCall, FunctionStateMutability, + GetBlockTransactionCountParams, GetBlockWithReceiptsParams, GetBlockWithTxHashesParams, GetBlockWithTxsParams, + GetClassAtParams, GetClassHashAtParams, GetClassParams, GetEventsParams, GetNonceParams, GetStateUpdateParams, + GetStorageAtParams, GetStorageProofResult, GetTransactionByBlockIdAndIndexParams, GetTransactionByHashParams, + GetTransactionReceiptParams, GetTransactionStatusParams, GlobalRoots, InvokeTxn, InvokeTxnReceipt, + InvokeTxnV0, InvokeTxnV1, InvokeTxnV3, KeyValuePair, L1DaMode, L1HandlerTxn, L1HandlerTxnReceipt, + MaybeDeprecatedContractClass, MaybePreConfirmedBlockWithTxHashes, MaybePreConfirmedBlockWithTxs, + MessageFeeEstimate, + MerkleNode, MsgFromL1, MsgToL1, NewClasses, NodeHashToNodeMappingItem, NonceUpdate, PriceUnitFri, PriceUnitWei, + PreConfirmedBlockHeader, PreConfirmedBlockWithReceipts, PreConfirmedBlockWithTxHashes, + PreConfirmedBlockWithTxs, ReplacedClass, ResourceBounds, ResourceBoundsMapping, ResourcePrice, + SierraEntryPoint, Signature, SimulationFlagForEstimateFee, SpecVersionParams, StateUpdate, StorageKey, + StarknetGetBlockWithTxsAndReceiptsResult, StructAbiEntry, StructAbiType, StructMember, SyncStatus, + SyncingParams, SyncingStatus, TransactionAndReceipt, Txn, TxnExecutionStatus, TxnFinalityAndExecutionStatus, + TxnFinalityStatus, TxnHash, TxnReceipt, TxnReceiptWithBlockInfo, TxnStatus, TxnWithHash, TypedParameter, +}; +use serde::{Deserialize, Serialize}; +use starknet_types_core::felt::Felt; + +/// RPC 0.10.0 Changes: +/// 1. StateDiff: Added `migrated_compiled_classes` field +/// 2. PreConfirmedStateUpdate: Removed `old_root` field +/// 3. EmittedEvent: Added `transaction_index` and `event_index` fields +/// 4. ContractStorageKeysItem: Changed `storage_keys` type from `Vec` to `Vec` + +/// The change in state applied in this block, given as a mapping of addresses to the new values and/or new contracts +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct StateDiff { + /// The declared class hash and compiled class hash + pub declared_classes: Vec, + /// The deployed contracts + pub deployed_contracts: Vec, + /// The hash of the declared class + pub deprecated_declared_classes: Vec, + /// The nonce updates + pub nonces: Vec, + /// The replaced classes + pub replaced_classes: Vec, + /// The storage diffs + pub storage_diffs: Vec, + /// The migrated compiled classes (NEW in v0.10.0) + pub migrated_compiled_classes: Vec, +} + +/// A migrated class item representing a class that was migrated from Poseidon to BLAKE hash +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct MigratedClassItem { + /// The hash of the declared class + pub class_hash: Felt, + /// The new BLAKE hash (post-SNIP-34) + pub compiled_class_hash: Felt, +} + +#[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone, Debug)] +#[serde(untagged)] +pub enum MaybePreConfirmedStateUpdate { + Block(StateUpdate), + PreConfirmed(PreConfirmedStateUpdate), +} + +/// Pre-confirmed state update (v0.10.0: removed `old_root` field) +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct PreConfirmedStateUpdate { + /// The state diff + pub state_diff: StateDiff, +} + +/// An event emitted as part of a transaction (v0.10.0: added transaction_index and event_index) +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct EmittedEvent { + /// The event information + #[serde(flatten)] + pub event: Event, + /// The hash of the block in which the event was emitted + #[serde(default)] + pub block_hash: Option, + /// The number of the block in which the event was emitted + #[serde(default)] + pub block_number: Option, + /// The transaction that emitted the event + pub transaction_hash: TxnHash, + /// The index of the transaction within the block + pub transaction_index: u64, + /// The index of the event within the transaction + pub event_index: u64, +} + +/// Contract storage keys item (v0.10.0: changed storage_keys type from Vec to Vec) +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ContractStorageKeysItem { + /// The address of the contract + pub contract_address: Felt, + /// The storage keys (changed from Vec to Vec in v0.10.0) + pub storage_keys: Vec, +} diff --git a/madara/crates/primitives/rpc/src/v0_10_0/starknet_trace_api_openrpc.rs b/madara/crates/primitives/rpc/src/v0_10_0/starknet_trace_api_openrpc.rs new file mode 100644 index 0000000000..7bf42dbf25 --- /dev/null +++ b/madara/crates/primitives/rpc/src/v0_10_0/starknet_trace_api_openrpc.rs @@ -0,0 +1,6 @@ +pub use crate::v0_9_0::{ + CallType, DeclareTransactionTrace, DeployAccountTransactionTrace, EntryPointType, + InvokeTransactionTrace, L1HandlerTransactionTrace, OrderedEvent, OrderedMessage, + RevertedInvocation, RevertibleFunctionInvocation, SimulationFlag, SimulateTransactionsResult, + TraceBlockTransactionsResult, TraceTransactionResult, TransactionTrace, +}; diff --git a/madara/crates/primitives/rpc/src/v0_10_0/starknet_ws_api.rs b/madara/crates/primitives/rpc/src/v0_10_0/starknet_ws_api.rs new file mode 100644 index 0000000000..0e451b9855 --- /dev/null +++ b/madara/crates/primitives/rpc/src/v0_10_0/starknet_ws_api.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; + +use super::{EmittedEvent, TxnFinalityStatus}; + +pub use crate::v0_9_0::FinalityStatus; + +/// An emitted event with finality status for WebSocket subscriptions +#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)] +pub struct EmittedEventWithFinality { + #[serde(flatten)] + pub emitted_event: EmittedEvent, + #[serde(flatten)] + pub finality_status: TxnFinalityStatus, +} diff --git a/madara/crates/primitives/state_update/src/into_starknet_types.rs b/madara/crates/primitives/state_update/src/into_starknet_types.rs index ca23ec3b5a..6748a59338 100644 --- a/madara/crates/primitives/state_update/src/into_starknet_types.rs +++ b/madara/crates/primitives/state_update/src/into_starknet_types.rs @@ -58,6 +58,7 @@ impl From for StateDiff { .map(|replaced_class| replaced_class.into()) .collect(), nonces: state_diff.nonces.into_iter().map(|nonce| nonce.into()).collect(), + migrated_compiled_classes: vec![], // v0.7.1 doesn't have migrated classes } } } @@ -171,6 +172,61 @@ impl From for mp_rpc::v0_7_1::NonceUpdate { } } +// v0.10.0 conversions +impl From for mp_rpc::v0_10_0::StateDiff { + fn from(state_diff: StateDiff) -> Self { + Self { + storage_diffs: state_diff + .storage_diffs + .into_iter() + .map(|diff| mp_rpc::v0_10_0::ContractStorageDiffItem { + address: diff.address, + storage_entries: diff + .storage_entries + .into_iter() + .map(|entry| mp_rpc::v0_10_0::KeyValuePair { key: entry.key, value: entry.value }) + .collect(), + }) + .collect(), + deprecated_declared_classes: state_diff.old_declared_contracts, + declared_classes: state_diff + .declared_classes + .into_iter() + .map(|item| mp_rpc::v0_10_0::NewClasses { + class_hash: item.class_hash, + compiled_class_hash: item.compiled_class_hash, + }) + .collect(), + deployed_contracts: state_diff + .deployed_contracts + .into_iter() + .map(|item| mp_rpc::v0_10_0::DeployedContractItem { address: item.address, class_hash: item.class_hash }) + .collect(), + replaced_classes: state_diff + .replaced_classes + .into_iter() + .map(|item| mp_rpc::v0_10_0::ReplacedClass { + contract_address: item.contract_address, + class_hash: item.class_hash, + }) + .collect(), + nonces: state_diff + .nonces + .into_iter() + .map(|item| mp_rpc::v0_10_0::NonceUpdate { contract_address: item.contract_address, nonce: item.nonce }) + .collect(), + migrated_compiled_classes: state_diff + .migrated_compiled_classes + .into_iter() + .map(|item| mp_rpc::v0_10_0::MigratedClassItem { + class_hash: item.class_hash, + compiled_class_hash: item.compiled_class_hash, + }) + .collect(), + } + } +} + #[cfg(test)] mod test { use super::*; diff --git a/madara/crates/primitives/state_update/src/lib.rs b/madara/crates/primitives/state_update/src/lib.rs index deba741560..1c07a0e5e1 100644 --- a/madara/crates/primitives/state_update/src/lib.rs +++ b/madara/crates/primitives/state_update/src/lib.rs @@ -144,6 +144,7 @@ impl TransactionStateUpdate { self.nonces.iter().map(|(&contract_address, &nonce)| NonceUpdate { contract_address, nonce }).collect(), |entry| entry.contract_address, ), + migrated_compiled_classes: Vec::new(), // TODO(prakhar,22/11/2025): Add migrated compiled classes here } } } @@ -188,6 +189,8 @@ pub struct StateDiff { pub replaced_classes: Vec, /// New contract nonce. Mapping contract_address => nonce. pub nonces: Vec, + /// Classes that were migrated from Poseidon to BLAKE hash (SNIP-34). Mapping class_hash => compiled_class_hash. + pub migrated_compiled_classes: Vec, } impl StateDiff { @@ -198,6 +201,7 @@ impl StateDiff { && self.nonces.is_empty() && self.replaced_classes.is_empty() && self.storage_diffs.is_empty() + && self.migrated_compiled_classes.is_empty() } pub fn len(&self) -> usize { @@ -207,6 +211,7 @@ impl StateDiff { result += self.old_declared_contracts.len(); result += self.nonces.len(); result += self.replaced_classes.len(); + result += self.migrated_compiled_classes.len(); for storage_diff in &self.storage_diffs { result += storage_diff.len(); @@ -222,6 +227,7 @@ impl StateDiff { self.deployed_contracts.sort_by_key(|deployed_contract| deployed_contract.address); self.replaced_classes.sort_by_key(|replaced_class| replaced_class.contract_address); self.nonces.sort_by_key(|nonce| nonce.contract_address); + self.migrated_compiled_classes.sort_by_key(|migrated_class| migrated_class.class_hash); } pub fn compute_hash(&self) -> Felt { @@ -265,11 +271,18 @@ impl StateDiff { storage_diffs }; + let migrated_compiled_classes_sorted = { + let mut migrated_compiled_classes = self.migrated_compiled_classes.clone(); + migrated_compiled_classes.sort_by_key(|migrated_class| migrated_class.class_hash); + migrated_compiled_classes + }; + let updated_contracts_len_as_felt = (updated_contracts_sorted.len() as u64).into(); let declared_classes_len_as_felt = (declared_classes_sorted.len() as u64).into(); let deprecated_declared_classes_len_as_felt = (deprecated_declared_classes_sorted.len() as u64).into(); let nonces_len_as_felt = (nonces_sorted.len() as u64).into(); let storage_diffs_len_as_felt = (storage_diffs_sorted.len() as u64).into(); + let migrated_compiled_classes_len_as_felt = (migrated_compiled_classes_sorted.len() as u64).into(); let elements: Vec = std::iter::once(Felt::from_bytes_be_slice(b"STARKNET_STATE_DIFF0")) .chain(std::iter::once(updated_contracts_len_as_felt)) @@ -297,6 +310,12 @@ impl StateDiff { })) .chain(std::iter::once(nonces_len_as_felt)) .chain(nonces_sorted.into_iter().flat_map(|nonce| vec![nonce.contract_address, nonce.nonce])) + .chain(std::iter::once(migrated_compiled_classes_len_as_felt)) + .chain( + migrated_compiled_classes_sorted + .into_iter() + .flat_map(|migrated_class| vec![migrated_class.class_hash, migrated_class.compiled_class_hash]), + ) .collect(); Poseidon::hash_array(&elements) @@ -360,6 +379,7 @@ impl From for StateDiff { deployed_contracts, replaced_classes, nonces, + migrated_compiled_classes: Vec::new(), // Migrated classes are handled separately } } } @@ -407,6 +427,13 @@ pub struct ReplacedClassItem { pub class_hash: Felt, } +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub struct MigratedClassItem { + pub class_hash: Felt, + pub compiled_class_hash: Felt, +} + #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct NonceUpdate { pub contract_address: Felt, @@ -499,6 +526,7 @@ mod tests { NonceUpdate { contract_address: Felt::from(25), nonce: Felt::from(26) }, NonceUpdate { contract_address: Felt::from(27), nonce: Felt::from(28) }, ], + migrated_compiled_classes: vec![], // TODO(prakhar,22/11/2025): Add value here and update the test } } }