diff --git a/crates/starknet-devnet-core/src/messaging/ethereum.rs b/crates/starknet-devnet-core/src/messaging/ethereum.rs index 8b18c0f7f..0690f15a0 100644 --- a/crates/starknet-devnet-core/src/messaging/ethereum.rs +++ b/crates/starknet-devnet-core/src/messaging/ethereum.rs @@ -7,7 +7,7 @@ use ethers::prelude::*; use ethers::providers::{Http, Provider, ProviderError}; use ethers::types::{Address, BlockNumber, Log}; use k256::ecdsa::SigningKey; -use starknet_rs_core::types::Felt; +use starknet_rs_core::types::{Felt, Hash256}; use starknet_types::felt::felt_from_prefixed_hex; use starknet_types::rpc::contract_address::ContractAddress; use starknet_types::rpc::messaging::{MessageToL1, MessageToL2}; @@ -302,6 +302,7 @@ impl EthereumMessaging { /// /// * `log` - The log to be converted. pub fn message_to_l2_from_log(log: Log) -> DevnetResult { + let l1_transaction_hash = log.transaction_hash.map(|h| Hash256::from_bytes(h.to_fixed_bytes())); let parsed_log = ::decode_log(&log.into()).map_err(|e| { Error::MessagingError(MessagingError::EthersError(format!("Log parsing failed {}", e))) })?; @@ -318,6 +319,7 @@ pub fn message_to_l2_from_log(log: Log) -> DevnetResult { } Ok(MessageToL2 { + l1_transaction_hash, l2_contract_address: contract_address, entry_point_selector, l1_contract_address: ContractAddress::new(from_address)?, @@ -405,6 +407,7 @@ mod tests { }; let expected_message = MessageToL2 { + l1_transaction_hash: None, l1_contract_address: ContractAddress::new( felt_from_prefixed_hex(from_address).unwrap(), ) diff --git a/crates/starknet-devnet-core/src/messaging/mod.rs b/crates/starknet-devnet-core/src/messaging/mod.rs index 6937dc45e..db5943677 100644 --- a/crates/starknet-devnet-core/src/messaging/mod.rs +++ b/crates/starknet-devnet-core/src/messaging/mod.rs @@ -32,7 +32,8 @@ //! contract (`mockSendMessageFromL2` entrypoint). use std::collections::HashMap; -use starknet_rs_core::types::{BlockId, ExecutionResult, Hash256}; +use ethers::types::H256; +use starknet_rs_core::types::{BlockId, ExecutionResult, Felt, Hash256}; use starknet_types::rpc::messaging::{MessageToL1, MessageToL2}; use crate::error::{DevnetResult, Error, MessagingError}; @@ -54,13 +55,11 @@ pub struct MessagingBroker { /// For each time a message is supposed to be sent to L1, it is stored in this /// queue. The user may consume those messages using `consume_message_from_l2` /// to actually test `MessageToL1` emitted without running L1 node. - /// - /// Note: - /// `Hash256` is not directly supported as a HashMap key due to missing trait. - /// Using `String` instead. - pub l2_to_l1_messages_hashes: HashMap, + pub l2_to_l1_messages_hashes: HashMap, /// This list of messages that will be sent to L1 node at the next `postman/flush`. pub l2_to_l1_messages_to_flush: Vec, + /// Mapping of L1 transaction hash to a chronological sequence of generated L2 transactions. + pub l1_to_l2_tx_hashes: HashMap>, } impl MessagingBroker { @@ -142,7 +141,7 @@ impl Starknet { } for message in &messages { - let hash = format!("{}", message.hash()); + let hash = H256(*message.hash().as_bytes()); let count = self.messaging.l2_to_l1_messages_hashes.entry(hash).or_insert(0); *count += 1; } @@ -188,14 +187,14 @@ impl Starknet { // Ensure latest messages are collected before consuming the message. self.collect_messages_to_l1().await?; - let hash = format!("{}", message.hash()); - let count = self.messaging.l2_to_l1_messages_hashes.entry(hash.clone()).or_insert(0); + let hash = H256(*message.hash().as_bytes()); + let count = self.messaging.l2_to_l1_messages_hashes.entry(hash).or_insert(0); if *count > 0 { *count -= 1; Ok(message.hash()) } else { - Err(Error::MessagingError(MessagingError::MessageToL1NotPresent(hash))) + Err(Error::MessagingError(MessagingError::MessageToL1NotPresent(hash.to_string()))) } } diff --git a/crates/starknet-devnet-core/src/starknet/add_l1_handler_transaction.rs b/crates/starknet-devnet-core/src/starknet/add_l1_handler_transaction.rs index 60c7bde80..c6d2bf006 100644 --- a/crates/starknet-devnet-core/src/starknet/add_l1_handler_transaction.rs +++ b/crates/starknet-devnet-core/src/starknet/add_l1_handler_transaction.rs @@ -1,4 +1,5 @@ use blockifier::transaction::transactions::ExecutableTransaction; +use ethers::types::H256; use starknet_types::felt::TransactionHash; use starknet_types::rpc::transactions::l1_handler_transaction::L1HandlerTransaction; use starknet_types::rpc::transactions::{Transaction, TransactionWithHash}; @@ -34,6 +35,17 @@ pub fn add_l1_handler_transaction( blockifier_execution_result, )?; + // If L1 tx hash present, store the generated L2 tx hash in its messaging entry. + // Not done as part of `handle_transaction_result` as it is specific to this tx type. + if let Some(l1_tx_hash) = transaction.l1_transaction_hash { + starknet + .messaging + .l1_to_l2_tx_hashes + .entry(H256(*l1_tx_hash.as_bytes())) + .or_default() + .push(transaction_hash); + } + Ok(transaction_hash) } diff --git a/crates/starknet-devnet-core/src/starknet/mod.rs b/crates/starknet-devnet-core/src/starknet/mod.rs index 7bcec217d..9d8999598 100644 --- a/crates/starknet-devnet-core/src/starknet/mod.rs +++ b/crates/starknet-devnet-core/src/starknet/mod.rs @@ -10,6 +10,7 @@ use blockifier::transaction::account_transaction::AccountTransaction; use blockifier::transaction::errors::TransactionPreValidationError; use blockifier::transaction::objects::TransactionExecutionInfo; use blockifier::transaction::transactions::ExecutableTransaction; +use ethers::types::H256; use parking_lot::RwLock; use starknet_api::block::{BlockNumber, BlockStatus, BlockTimestamp, GasPrice, GasPricePerToken}; use starknet_api::core::SequencerContractAddress; @@ -17,7 +18,7 @@ use starknet_api::felt; use starknet_api::transaction::Fee; use starknet_config::BlockGenerationOn; use starknet_rs_core::types::{ - BlockId, BlockTag, Call, ExecutionResult, Felt, MsgFromL1, TransactionExecutionStatus, + BlockId, BlockTag, Call, ExecutionResult, Felt, Hash256, MsgFromL1, TransactionExecutionStatus, TransactionFinalityStatus, }; use starknet_rs_core::utils::get_selector_from_name; @@ -47,8 +48,8 @@ use starknet_types::rpc::transactions::l1_handler_transaction::L1HandlerTransact use starknet_types::rpc::transactions::{ BlockTransactionTrace, BroadcastedDeclareTransaction, BroadcastedDeployAccountTransaction, BroadcastedInvokeTransaction, BroadcastedTransaction, BroadcastedTransactionCommon, - SimulatedTransaction, SimulationFlag, TransactionTrace, TransactionType, TransactionWithHash, - TransactionWithReceipt, Transactions, + L1HandlerTransactionStatus, SimulatedTransaction, SimulationFlag, TransactionTrace, + TransactionType, TransactionWithHash, TransactionWithReceipt, Transactions, }; use starknet_types::traits::HashProducer; use tracing::{error, info}; @@ -1380,6 +1381,30 @@ impl Starknet { Ok(false) } } + + pub fn get_messages_status( + &self, + l1_tx_hash: Hash256, + ) -> Option> { + match self.messaging.l1_to_l2_tx_hashes.get(&H256(*l1_tx_hash.as_bytes())) { + Some(l2_tx_hashes) => { + let mut statuses = vec![]; + for l2_tx_hash in l2_tx_hashes { + match self.transactions.get(l2_tx_hash) { + Some(l2_tx) => statuses.push(L1HandlerTransactionStatus { + transaction_hash: *l2_tx_hash, + finality_status: l2_tx.finality_status, + failure_reason: l2_tx.execution_info.revert_error.clone(), + }), + // should never happen due to handling in add_l1_handler_transaction + None => return None, + } + } + Some(statuses) + } + None => None, + } + } } #[cfg(test)] diff --git a/crates/starknet-devnet-server/src/api/json_rpc/endpoints.rs b/crates/starknet-devnet-server/src/api/json_rpc/endpoints.rs index 207bb7943..8d8970b51 100644 --- a/crates/starknet-devnet-server/src/api/json_rpc/endpoints.rs +++ b/crates/starknet-devnet-server/src/api/json_rpc/endpoints.rs @@ -13,7 +13,9 @@ use starknet_types::rpc::transactions::{ use starknet_types::starknet_api::block::BlockStatus; use super::error::{ApiError, StrictRpcResult}; -use super::models::{BlockHashAndNumberOutput, SyncingOutput, TransactionStatusOutput}; +use super::models::{ + BlockHashAndNumberOutput, L1TransactionHashInput, SyncingOutput, TransactionStatusOutput, +}; use super::{DevnetResponse, JsonRpcHandler, JsonRpcResponse, StarknetResponse, RPC_SPEC_VERSION}; use crate::api::http::endpoints::accounts::{ get_account_balance_impl, get_predeployed_accounts_impl, BalanceQuery, PredeployedAccountsQuery, @@ -463,6 +465,18 @@ impl JsonRpcHandler { } } + /// starknet_getMessagesStatus + pub async fn get_messages_status( + &self, + L1TransactionHashInput { transaction_hash }: L1TransactionHashInput, + ) -> StrictRpcResult { + let starknet = self.api.starknet.lock().await; + match starknet.get_messages_status(transaction_hash) { + Some(statuses) => Ok(StarknetResponse::MessagesStatusByL1Hash(statuses).into()), + None => Err(ApiError::TransactionNotFound), + } + } + /// devnet_getPredeployedAccounts pub async fn get_predeployed_accounts( &self, diff --git a/crates/starknet-devnet-server/src/api/json_rpc/mod.rs b/crates/starknet-devnet-server/src/api/json_rpc/mod.rs index 4fcc71b78..721b6af7e 100644 --- a/crates/starknet-devnet-server/src/api/json_rpc/mod.rs +++ b/crates/starknet-devnet-server/src/api/json_rpc/mod.rs @@ -13,7 +13,8 @@ use enum_helper_macros::{AllVariantsSerdeRenames, VariantName}; use futures::StreamExt; use models::{ BlockAndClassHashInput, BlockAndContractAddressInput, BlockAndIndexInput, CallInput, - EstimateFeeInput, EventsInput, GetStorageInput, TransactionHashInput, TransactionHashOutput, + EstimateFeeInput, EventsInput, GetStorageInput, L1TransactionHashInput, TransactionHashInput, + TransactionHashOutput, }; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -28,7 +29,8 @@ use starknet_types::rpc::gas_modification::{GasModification, GasModificationRequ use starknet_types::rpc::state::{PendingStateUpdate, StateUpdate}; use starknet_types::rpc::transaction_receipt::TransactionReceipt; use starknet_types::rpc::transactions::{ - BlockTransactionTrace, EventsChunk, SimulatedTransaction, TransactionTrace, TransactionWithHash, + BlockTransactionTrace, EventsChunk, L1HandlerTransactionStatus, SimulatedTransaction, + TransactionTrace, TransactionWithHash, }; use starknet_types::starknet_api::block::BlockNumber; use tracing::{error, info, trace}; @@ -337,6 +339,7 @@ impl JsonRpcHandler { JsonRpcRequest::AccountBalance(data) => self.get_account_balance(data).await, JsonRpcRequest::Mint(data) => self.mint(data).await, JsonRpcRequest::DevnetConfig => self.get_devnet_config().await, + JsonRpcRequest::MessagesStatusByL1Hash(data) => self.get_messages_status(data).await, }; // If locally we got an error and forking is set up, forward the request to the origin @@ -476,6 +479,8 @@ pub enum JsonRpcRequest { TransactionReceiptByTransactionHash(TransactionHashInput), #[serde(rename = "starknet_getTransactionStatus")] TransactionStatusByHash(TransactionHashInput), + #[serde(rename = "starknet_getMessagesStatus")] + MessagesStatusByL1Hash(L1TransactionHashInput), #[serde(rename = "starknet_getClass")] ClassByHash(BlockAndClassHashInput), #[serde(rename = "starknet_getClassHashAt")] @@ -609,6 +614,7 @@ pub enum StarknetResponse { SimulateTransactions(Vec), TraceTransaction(TransactionTrace), BlockTransactionTraces(Vec), + MessagesStatusByL1Hash(Vec), } #[derive(Serialize)] diff --git a/crates/starknet-devnet-server/src/api/json_rpc/models.rs b/crates/starknet-devnet-server/src/api/json_rpc/models.rs index 01a809b31..485515d17 100644 --- a/crates/starknet-devnet-server/src/api/json_rpc/models.rs +++ b/crates/starknet-devnet-server/src/api/json_rpc/models.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use starknet_rs_core::types::{TransactionExecutionStatus, TransactionFinalityStatus}; +use starknet_rs_core::types::{Hash256, TransactionExecutionStatus, TransactionFinalityStatus}; use starknet_types::contract_address::ContractAddress; use starknet_types::felt::{BlockHash, ClassHash, TransactionHash}; use starknet_types::patricia_key::PatriciaKey; @@ -172,6 +172,12 @@ pub struct TransactionStatusOutput { pub execution_status: TransactionExecutionStatus, } +#[derive(Debug, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct L1TransactionHashInput { + pub transaction_hash: Hash256, +} + #[cfg(test)] mod tests { use starknet_rs_core::types::{BlockId as ImportedBlockId, BlockTag, Felt}; diff --git a/crates/starknet-devnet-types/src/rpc/messaging.rs b/crates/starknet-devnet-types/src/rpc/messaging.rs index ba33bd496..5eeb08d1f 100644 --- a/crates/starknet-devnet-types/src/rpc/messaging.rs +++ b/crates/starknet-devnet-types/src/rpc/messaging.rs @@ -10,6 +10,7 @@ use crate::rpc::eth_address::EthAddressWrapper; #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(deny_unknown_fields)] pub struct MessageToL2 { + pub l1_transaction_hash: Option, pub l2_contract_address: ContractAddress, pub entry_point_selector: EntryPointSelector, pub l1_contract_address: ContractAddress, diff --git a/crates/starknet-devnet-types/src/rpc/transactions.rs b/crates/starknet-devnet-types/src/rpc/transactions.rs index 6f3c775ca..a6c5019bf 100644 --- a/crates/starknet-devnet-types/src/rpc/transactions.rs +++ b/crates/starknet-devnet-types/src/rpc/transactions.rs @@ -1139,6 +1139,14 @@ impl FunctionInvocation { } } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct L1HandlerTransactionStatus { + pub transaction_hash: TransactionHash, + pub finality_status: TransactionFinalityStatus, + pub failure_reason: Option, +} + #[cfg(test)] mod tests { use starknet_rs_crypto::poseidon_hash_many; diff --git a/crates/starknet-devnet-types/src/rpc/transactions/l1_handler_transaction.rs b/crates/starknet-devnet-types/src/rpc/transactions/l1_handler_transaction.rs index 4fe79d833..272d57c00 100644 --- a/crates/starknet-devnet-types/src/rpc/transactions/l1_handler_transaction.rs +++ b/crates/starknet-devnet-types/src/rpc/transactions/l1_handler_transaction.rs @@ -11,7 +11,7 @@ use starknet_api::transaction::{ TransactionHash as ApiTransactionHash, TransactionVersion as ApiTransactionVersion, }; use starknet_rs_core::crypto::compute_hash_on_elements; -use starknet_rs_core::types::Felt; +use starknet_rs_core::types::{Felt, Hash256}; use super::{deserialize_paid_fee_on_l1, serialize_paid_fee_on_l1}; use crate::constants::PREFIX_L1_HANDLER; @@ -23,6 +23,9 @@ use crate::rpc::messaging::MessageToL2; #[derive(Debug, Clone, Default, Eq, PartialEq, Deserialize, Serialize)] #[serde(deny_unknown_fields)] pub struct L1HandlerTransaction { + /// The hash of the L1 transaction that triggered this L1 handler execution. + /// Omissible if received via mock (devnet_postmanSendMessageToL2) + pub l1_transaction_hash: Option, pub version: TransactionVersion, pub nonce: Nonce, pub contract_address: ContractAddress, @@ -102,9 +105,8 @@ impl L1HandlerTransaction { calldata, nonce: message.nonce, paid_fee_on_l1, - // Currently, only version 0 is supported, which - // is ensured by default initialization. - ..Default::default() + l1_transaction_hash: message.l1_transaction_hash, + version: Felt::ZERO, // currently, only version 0 is supported }) } } @@ -122,6 +124,7 @@ impl TryFrom<&L1HandlerTransaction> for MessageToL2 { let payload = value.calldata[1..].to_vec(); Ok(MessageToL2 { + l1_transaction_hash: value.l1_transaction_hash, l2_contract_address: value.contract_address, entry_point_selector: value.entry_point_selector, l1_contract_address: ContractAddress::new(*l1_contract_address)?, @@ -158,6 +161,7 @@ mod tests { vec![felt_from_prefixed_hex(from_address).unwrap(), Felt::ONE, Felt::TWO]; let message = MessageToL2 { + l1_transaction_hash: None, l1_contract_address: ContractAddress::new( felt_from_prefixed_hex(from_address).unwrap(), ) @@ -170,8 +174,6 @@ mod tests { paid_fee_on_l1: fee.into(), }; - let chain_id = ChainId::goerli_legacy_id(); - let transaction_hash = felt_from_prefixed_hex( "0x6182c63599a9638272f1ce5b5cadabece9c81c2d2b8f88ab7a294472b8fce8b", ) @@ -199,6 +201,6 @@ mod tests { let transaction = L1HandlerTransaction::try_from_message_to_l2(message).unwrap(); assert_eq!(transaction, expected_tx); - assert_eq!(transaction.compute_hash(chain_id), transaction_hash); + assert_eq!(transaction.compute_hash(ChainId::goerli_legacy_id()), transaction_hash); } } diff --git a/crates/starknet-devnet/tests/test_messaging.rs b/crates/starknet-devnet/tests/test_messaging.rs index 1cff7f424..dbc7989b9 100644 --- a/crates/starknet-devnet/tests/test_messaging.rs +++ b/crates/starknet-devnet/tests/test_messaging.rs @@ -780,4 +780,127 @@ mod test_messaging { devnet.send_custom_rpc("devnet_postmanFlush", json!({})).await.unwrap(); assert_eq!(get_balance(&devnet, sn_l1l2_contract, user_sn).await, [Felt::ONE]); } + + #[tokio::test] + async fn test_getting_status_of_mock_message() { + let (devnet, _, l1l2_contract_address) = setup_devnet(&[]).await; + + // Use postman to send a message to l2 without l1 - the message increments user balance + let increment_amount = Felt::from(0xff); + + let user = Felt::ONE; + let l1_tx_hash = Felt::from(0xabc); + let mock_msg_body = json!({ + "l1_contract_address": MESSAGING_L1_ADDRESS, + "l2_contract_address": l1l2_contract_address, + "entry_point_selector": get_selector_from_name("deposit").unwrap(), + "payload": [user, increment_amount], + "paid_fee_on_l1": "0x1234", + "nonce": "0x1", + "l1_transaction_hash": l1_tx_hash, + }); + + let mock_msg_resp = + devnet.send_custom_rpc("devnet_postmanSendMessageToL2", mock_msg_body).await.unwrap(); + assert_eq!(get_balance(&devnet, l1l2_contract_address, user).await, [increment_amount]); + + let messages_status = devnet + .send_custom_rpc( + "starknet_getMessagesStatus", + json!({ "transaction_hash": l1_tx_hash }), + ) + .await + .unwrap(); + assert_eq!( + messages_status, + json!([{ + "transaction_hash": mock_msg_resp["transaction_hash"], + "finality_status": "ACCEPTED_ON_L2", + "failure_reason": null, + }]) + ); + } + + #[tokio::test] + async fn test_getting_status_of_real_message() { + let anvil = BackgroundAnvil::spawn().await.unwrap(); + let (devnet, sn_account, sn_l1l2_contract) = setup_devnet(&[]).await; + + // Load l1 messaging contract. + let body: serde_json::Value = devnet + .send_custom_rpc("devnet_postmanLoad", json!({ "network_url": anvil.url })) + .await + .expect("deploy l1 messaging contract failed"); + + assert_eq!( + body.get("messaging_contract_address").unwrap().as_str().unwrap(), + MESSAGING_L1_ADDRESS + ); + + // Deploy the L1L2 testing contract on L1 (on L2 it's already pre-deployed). + let l1_messaging_address = H160::from_str(MESSAGING_L1_ADDRESS).unwrap(); + let eth_l1l2_address = anvil.deploy_l1l2_contract(l1_messaging_address).await.unwrap(); + + let eth_l1l2_address_hex = format!("{eth_l1l2_address:#x}"); + let eth_l1l2_address_felt = felt_from_prefixed_hex(ð_l1l2_address_hex).unwrap(); + + // Set balance to 1 for the user 1 on L2 and withdraw to L1. + let user_sn = Felt::ONE; + let user_balance = Felt::ONE; + increase_balance(sn_account.clone(), sn_l1l2_contract, user_sn, user_balance).await; + withdraw(sn_account, sn_l1l2_contract, user_sn, user_balance, eth_l1l2_address_felt).await; + + // Flush to send the messages. + devnet.send_custom_rpc("devnet_postmanFlush", json!({})).await.unwrap(); + + let user_eth = 1.into(); + let sn_l1l2_contract_u256 = felt_to_u256(sn_l1l2_contract); + + // Consume the message to increase the balance on L1 + anvil + .withdraw_l1l2(eth_l1l2_address, sn_l1l2_contract_u256, user_eth, 1.into()) + .await + .unwrap(); + + // Send back the amount 1 to the user 1 on L2. + anvil + .deposit_l1l2(eth_l1l2_address, sn_l1l2_contract_u256, user_eth, 1.into()) + .await + .unwrap(); + + // Flush to trigger L2 transaction generation. + let generated_l2_txs_raw = + &devnet.send_custom_rpc("devnet_postmanFlush", json!({})).await.unwrap() + ["generated_l2_transactions"]; + let generated_l2_txs = generated_l2_txs_raw.as_array().unwrap(); + assert_eq!(generated_l2_txs.len(), 1); + let generated_l2_tx = &generated_l2_txs[0]; + + let latest_l1_txs = anvil + .provider + .get_block(ethers::types::BlockId::Number(ethers::types::BlockNumber::Latest)) + .await + .unwrap() + .unwrap() + .transactions; + + assert_eq!(latest_l1_txs.len(), 1); + let latest_l1_tx = latest_l1_txs[0]; + + let messages_status = devnet + .send_custom_rpc( + "starknet_getMessagesStatus", + json!({ "transaction_hash": latest_l1_tx }), + ) + .await + .unwrap(); + assert_eq!( + messages_status, + json!([{ + "transaction_hash": generated_l2_tx, + "finality_status": "ACCEPTED_ON_L2", + "failure_reason": null, + }]) + ) + } } diff --git a/website/docs/postman.md b/website/docs/postman.md index 8d59a4e05..5710cf230 100644 --- a/website/docs/postman.md +++ b/website/docs/postman.md @@ -119,7 +119,7 @@ A running L1 node is **not** required for this operation. ::: -Sends a mock transactions to L2, as if coming from L1, without the need for running L1. The deployed L2 contract address `l2_contract_address` and `entry_point_selector` must be valid, otherwise a new block will not be created. +Sends a mock transactions to L2, as if coming from L1, without the need for running L1. The deployed L2 contract address `l2_contract_address` and `entry_point_selector` must be valid, otherwise a new block will not be created. The `l1_transaction_hash` property is optional and, if provided, enables future `starknet_getMessagesStatus` requests with that hash value provided. Normally `nonce` is calculated by the L1 Starknet contract and it is used in L1 and L2. In this case, it needs to be provided manually. @@ -139,7 +139,8 @@ Request: "0x2" ], "paid_fee_on_l1": "0x123456abcdef", - "nonce":"0x0" + "nonce":"0x0", + "l1_transaction_hash": "0x000abc123", // optional } ``` @@ -158,7 +159,8 @@ JSON-RPC "0x2" ], "paid_fee_on_l1": "0x123456abcdef", - "nonce":"0x0" + "nonce":"0x0", + "l1_transaction_hash": "0x000abc123", // optional } } ```