starknet_devnet_server/api/
write_endpoints.rs

1use starknet_rs_core::types::TransactionExecutionStatus;
2use starknet_types::contract_address::ContractAddress;
3use starknet_types::felt::{TransactionHash, felt_from_prefixed_hex};
4use starknet_types::messaging::{MessageToL1, MessageToL2};
5use starknet_types::rpc::block::{BlockId, BlockTag};
6use starknet_types::rpc::gas_modification::GasModificationRequest;
7use starknet_types::rpc::transaction_receipt::FeeUnit;
8use starknet_types::rpc::transactions::l1_handler_transaction::L1HandlerTransaction;
9use starknet_types::rpc::transactions::{
10    BroadcastedDeclareTransaction, BroadcastedDeployAccountTransaction,
11    BroadcastedInvokeTransaction,
12};
13
14use super::error::{ApiError, StrictRpcResult};
15use super::models::{
16    DeclareTransactionOutput, DeployAccountTransactionOutput, DevnetResponse, JsonRpcResponse,
17    StarknetResponse, TransactionHashOutput,
18};
19use crate::api::JsonRpcHandler;
20use crate::api::account_helpers::{get_balance, get_erc20_fee_unit_address};
21use crate::api::models::{
22    AbortedBlocks, AbortingBlocks, AcceptOnL1Request, AcceptedOnL1Blocks, CreatedBlock, DumpPath,
23    FlushParameters, FlushedMessages, IncreaseTime, IncreaseTimeResponse, MessageHash,
24    MessagingLoadAddress, MintTokensRequest, MintTokensResponse, PostmanLoadL1MessagingContract,
25    RestartParameters, SetTime, SetTimeResponse,
26};
27use crate::dump_util::{dump_events, load_events};
28use crate::rpc_core::error::RpcError;
29use crate::rpc_core::request::RpcMethodCall;
30use crate::rpc_core::response::ResponseResult;
31use crate::rpc_handler::RpcHandler;
32
33impl JsonRpcHandler {
34    pub async fn add_declare_transaction(
35        &self,
36        request: BroadcastedDeclareTransaction,
37    ) -> StrictRpcResult {
38        let (transaction_hash, class_hash) =
39            self.api.starknet.lock().await.add_declare_transaction(request).map_err(
40                |err| match err {
41                    starknet_core::error::Error::CompiledClassHashMismatch => {
42                        ApiError::CompiledClassHashMismatch
43                    }
44                    starknet_core::error::Error::ClassAlreadyDeclared { .. } => {
45                        ApiError::ClassAlreadyDeclared
46                    }
47                    starknet_core::error::Error::ContractClassSizeIsTooLarge => {
48                        ApiError::ContractClassSizeIsTooLarge
49                    }
50                    unknown_error => ApiError::StarknetDevnetError(unknown_error),
51                },
52            )?;
53
54        Ok(StarknetResponse::AddDeclareTransaction(DeclareTransactionOutput {
55            transaction_hash,
56            class_hash,
57        })
58        .into())
59    }
60
61    pub async fn add_deploy_account_transaction(
62        &self,
63        request: BroadcastedDeployAccountTransaction,
64    ) -> StrictRpcResult {
65        let (transaction_hash, contract_address) =
66            self.api.starknet.lock().await.add_deploy_account_transaction(request).map_err(
67                |err| match err {
68                    starknet_core::error::Error::StateError(
69                        starknet_core::error::StateError::NoneClassHash(_),
70                    ) => ApiError::ClassHashNotFound,
71                    unknown_error => ApiError::StarknetDevnetError(unknown_error),
72                },
73            )?;
74
75        Ok(StarknetResponse::AddDeployAccountTransaction(DeployAccountTransactionOutput {
76            transaction_hash,
77            contract_address,
78        })
79        .into())
80    }
81
82    pub async fn add_invoke_transaction(
83        &self,
84        request: BroadcastedInvokeTransaction,
85    ) -> StrictRpcResult {
86        let transaction_hash = self.api.starknet.lock().await.add_invoke_transaction(request)?;
87
88        Ok(StarknetResponse::TransactionHash(TransactionHashOutput { transaction_hash }).into())
89    }
90
91    /// devnet_impersonateAccount
92    pub async fn impersonate_account(&self, address: ContractAddress) -> StrictRpcResult {
93        let mut starknet = self.api.starknet.lock().await;
94        starknet.impersonate_account(address)?;
95        Ok(JsonRpcResponse::Empty)
96    }
97
98    /// devnet_stopImpersonateAccount
99    pub async fn stop_impersonating_account(&self, address: ContractAddress) -> StrictRpcResult {
100        let mut starknet = self.api.starknet.lock().await;
101        starknet.stop_impersonating_account(&address);
102        Ok(JsonRpcResponse::Empty)
103    }
104
105    /// devnet_autoImpersonate | devnet_stopAutoImpersonate
106    pub async fn set_auto_impersonate(&self, auto_impersonation: bool) -> StrictRpcResult {
107        let mut starknet = self.api.starknet.lock().await;
108        starknet.set_auto_impersonate_account(auto_impersonation)?;
109        Ok(JsonRpcResponse::Empty)
110    }
111
112    /// devnet_dump
113    pub async fn dump(&self, path: Option<DumpPath>) -> StrictRpcResult {
114        if self.api.config.dump_on.is_none() {
115            return Err(ApiError::DumpError {
116                msg: "Please provide --dump-on mode on startup.".to_string(),
117            });
118        }
119
120        let path = path
121            .as_ref()
122            .map(|DumpPath { path }| path.clone())
123            .or_else(|| self.api.config.dump_path.clone())
124            .unwrap_or_default();
125
126        let dumpable_events = { self.api.dumpable_events.lock().await.clone() };
127
128        if !path.is_empty() {
129            dump_events(&dumpable_events, &path)
130                .map_err(|err| ApiError::DumpError { msg: err.to_string() })?;
131            return Ok(DevnetResponse::DevnetDump(None).into());
132        }
133
134        Ok(DevnetResponse::DevnetDump(Some(dumpable_events)).into())
135    }
136
137    /// devnet_load
138    pub async fn load(&self, path: String) -> StrictRpcResult {
139        let events = load_events(self.api.config.dump_on, &path)?;
140        // Necessary to restart before loading; restarting messaging to allow re-execution
141        self.restart(Some(RestartParameters { restart_l1_to_l2_messaging: true })).await?;
142        self.re_execute(&events).await.map_err(ApiError::RpcError)?;
143
144        Ok(JsonRpcResponse::Empty)
145    }
146
147    /// devnet_postmanLoad
148    pub async fn postman_load(&self, data: PostmanLoadL1MessagingContract) -> StrictRpcResult {
149        let mut starknet = self.api.starknet.lock().await;
150        let messaging_contract_address = starknet
151            .configure_messaging(
152                &data.network_url,
153                data.messaging_contract_address.as_deref(),
154                data.deployer_account_private_key.as_deref(),
155            )
156            .await?;
157
158        Ok(DevnetResponse::MessagingContractAddress(MessagingLoadAddress {
159            messaging_contract_address,
160        })
161        .into())
162    }
163
164    /// devnet_postmanFlush
165    pub async fn postman_flush(&self, data: Option<FlushParameters>) -> StrictRpcResult {
166        let is_dry_run = if let Some(params) = data { params.dry_run } else { false };
167
168        // Need to handle L1 to L2 first in case those messages create L2 to L1 messages.
169        let mut messages_to_l2 = vec![];
170        let mut generated_l2_transactions = vec![];
171        if !is_dry_run {
172            // Fetch and execute messages to L2.
173            // It is important that self.api.starknet is dropped immediately to allow rpc execution
174            messages_to_l2 =
175                self.api.starknet.lock().await.fetch_messages_to_l2().await.map_err(|e| {
176                    ApiError::RpcError(RpcError::internal_error_with(format!(
177                        "Error in fetching messages to L2: {e}"
178                    )))
179                })?;
180
181            for message in &messages_to_l2 {
182                let rpc_call = message.try_into().map_err(|e| {
183                    ApiError::RpcError(RpcError::internal_error_with(format!(
184                        "Error in converting message to L2 RPC call: {e}"
185                    )))
186                })?;
187                let tx_hash = execute_rpc_tx(self, rpc_call).await.map_err(ApiError::RpcError)?;
188                generated_l2_transactions.push(tx_hash);
189            }
190        };
191
192        // Collect and send messages to L1.
193        let mut starknet = self.api.starknet.lock().await;
194        let messages_to_l1 = starknet.collect_messages_to_l1().await.map_err(|e| {
195            ApiError::RpcError(RpcError::internal_error_with(format!(
196                "Error in collecting messages to L1: {e}"
197            )))
198        })?;
199
200        let l1_provider = if is_dry_run {
201            "dry run".to_string()
202        } else {
203            starknet.send_messages_to_l1().await.map_err(|e| {
204                ApiError::RpcError(RpcError::internal_error_with(format!(
205                    "Error in sending messages to L1: {e}"
206                )))
207            })?;
208            starknet.get_ethereum_url().unwrap_or("Not set".to_string())
209        };
210
211        let flushed_messages = FlushedMessages {
212            messages_to_l1,
213            messages_to_l2,
214            generated_l2_transactions,
215            l1_provider,
216        };
217
218        Ok(DevnetResponse::FlushedMessages(flushed_messages).into())
219    }
220
221    /// devnet_postmanSendMessageToL2
222    pub async fn postman_send_message_to_l2(&self, message: MessageToL2) -> StrictRpcResult {
223        let transaction = L1HandlerTransaction::try_from_message_to_l2(message)?;
224        let transaction_hash =
225            self.api.starknet.lock().await.add_l1_handler_transaction(transaction)?;
226        Ok(DevnetResponse::TransactionHash(TransactionHashOutput { transaction_hash }).into())
227    }
228
229    /// devnet_postmanConsumeMessageFromL2
230    pub async fn postman_consume_message_from_l2(&self, message: MessageToL1) -> StrictRpcResult {
231        let message_hash =
232            self.api.starknet.lock().await.consume_l2_to_l1_message(&message).await?;
233        Ok(DevnetResponse::MessageHash(MessageHash { message_hash }).into())
234    }
235
236    /// devnet_createBlock
237    pub async fn create_block(&self) -> StrictRpcResult {
238        let mut starknet = self.api.starknet.lock().await;
239
240        starknet.create_block()?;
241        let block = starknet.get_latest_block()?;
242
243        Ok(DevnetResponse::CreatedBlock(CreatedBlock { block_hash: block.block_hash() }).into())
244    }
245
246    /// devnet_abortBlocks
247    pub async fn abort_blocks(&self, data: AbortingBlocks) -> StrictRpcResult {
248        let aborted = self.api.starknet.lock().await.abort_blocks(data.starting_block_id)?;
249        Ok(DevnetResponse::AbortedBlocks(AbortedBlocks { aborted }).into())
250    }
251
252    /// devnet_acceptOnL1
253    pub async fn accept_on_l1(&self, data: AcceptOnL1Request) -> StrictRpcResult {
254        let accepted = self.api.starknet.lock().await.accept_on_l1(data.starting_block_id)?;
255        Ok(DevnetResponse::AcceptedOnL1Blocks(AcceptedOnL1Blocks { accepted }).into())
256    }
257
258    /// devnet_setGasPrice
259    pub async fn set_gas_price(&self, data: GasModificationRequest) -> StrictRpcResult {
260        let modified_gas =
261            self.api.starknet.lock().await.set_next_block_gas(data).map_err(ApiError::from)?;
262
263        Ok(DevnetResponse::GasModification(modified_gas).into())
264    }
265
266    /// devnet_restart
267    pub async fn restart(&self, data: Option<RestartParameters>) -> StrictRpcResult {
268        self.api.dumpable_events.lock().await.clear();
269
270        let restart_params = data.unwrap_or_default();
271        self.api.starknet.lock().await.restart(restart_params.restart_l1_to_l2_messaging)?;
272
273        self.api.sockets.lock().await.clear();
274
275        Ok(JsonRpcResponse::Empty)
276    }
277
278    /// devnet_setTime
279    pub async fn set_time(&self, data: SetTime) -> StrictRpcResult {
280        let mut starknet = self.api.starknet.lock().await;
281        let generate_block = data.generate_block.unwrap_or(true);
282        starknet.set_time(data.time, generate_block)?;
283        let block_hash = if generate_block {
284            let last_block = starknet.get_latest_block()?;
285            Some(last_block.block_hash())
286        } else {
287            None
288        };
289        Ok(DevnetResponse::SetTime(SetTimeResponse { block_timestamp: data.time, block_hash })
290            .into())
291    }
292
293    /// devnet_increaseTime
294    pub async fn increase_time(&self, data: IncreaseTime) -> StrictRpcResult {
295        let mut starknet = self.api.starknet.lock().await;
296        starknet.increase_time(data.time)?;
297
298        let last_block = starknet.get_latest_block()?;
299
300        Ok(DevnetResponse::IncreaseTime(IncreaseTimeResponse {
301            timestamp_increased_by: data.time,
302            block_hash: last_block.block_hash(),
303        })
304        .into())
305    }
306
307    /// devnet_mint
308    pub async fn mint(&self, request: MintTokensRequest) -> StrictRpcResult {
309        let mut starknet = self.api.starknet.lock().await;
310        let unit = request.unit.unwrap_or(FeeUnit::FRI);
311        let erc20_address = get_erc20_fee_unit_address(&unit);
312
313        // increase balance
314        let tx_hash = starknet.mint(request.address, request.amount, erc20_address).await?;
315
316        let tx = starknet.get_transaction_execution_and_finality_status(tx_hash)?;
317        match tx.execution_status {
318            TransactionExecutionStatus::Succeeded => {
319                let new_balance = get_balance(
320                    &mut starknet,
321                    request.address,
322                    erc20_address,
323                    BlockId::Tag(BlockTag::PreConfirmed),
324                )?;
325                let new_balance = new_balance.to_str_radix(10);
326
327                Ok(DevnetResponse::MintTokens(MintTokensResponse { new_balance, unit, tx_hash })
328                    .into())
329            }
330            TransactionExecutionStatus::Reverted => Err(ApiError::MintingReverted {
331                tx_hash,
332                revert_reason: tx.failure_reason.map(|reason| {
333                    if reason.contains("u256_add Overflow") {
334                        "The requested minting amount overflows the token contract's total_supply."
335                            .into()
336                    } else {
337                        reason
338                    }
339                }),
340            }),
341        }
342    }
343}
344
345async fn execute_rpc_tx(
346    rpc_handler: &JsonRpcHandler,
347    rpc_call: RpcMethodCall,
348) -> Result<TransactionHash, RpcError> {
349    match rpc_handler.on_call(rpc_call).await.result {
350        ResponseResult::Success(result) => {
351            let tx_hash_hex = result
352                .get("transaction_hash")
353                .ok_or(RpcError::internal_error_with(format!(
354                    "Message execution did not yield a transaction hash: {result:?}"
355                )))?
356                .as_str()
357                .ok_or(RpcError::internal_error_with(format!(
358                    "Message execution result contains invalid transaction hash: {result:?}"
359                )))?;
360            let tx_hash = felt_from_prefixed_hex(tx_hash_hex).map_err(|e| {
361                RpcError::internal_error_with(format!(
362                    "Message execution resulted in an invalid tx hash: {tx_hash_hex}: {e}"
363                ))
364            })?;
365            Ok(tx_hash)
366        }
367        ResponseResult::Error(e) => Err(e),
368    }
369}
370
371#[cfg(test)]
372mod tests {
373    use crate::api::models::BroadcastedDeployAccountTransactionEnumWrapper;
374
375    #[test]
376    fn check_correct_deserialization_of_deploy_account_transaction_request() {
377        let _: BroadcastedDeployAccountTransactionEnumWrapper = serde_json::from_str(
378            r#"{
379                "type":"DEPLOY_ACCOUNT",
380                "resource_bounds": {
381                    "l1_gas": {
382                        "max_amount": "0x1",
383                        "max_price_per_unit": "0x2"
384                    },
385                    "l1_data_gas": {
386                        "max_amount": "0x1",
387                        "max_price_per_unit": "0x2"
388                    },
389                    "l2_gas": {
390                        "max_amount": "0x1",
391                        "max_price_per_unit": "0x2"
392                    }
393                },
394                "tip": "0xabc",
395                "paymaster_data": [],
396                "version": "0x3",
397                "signature": ["0xFF", "0xAA"],
398                "nonce": "0x0",
399                "contract_address_salt": "0x01",
400                "class_hash": "0x01",
401                "constructor_calldata": ["0x01"],
402                "nonce_data_availability_mode": "L1",
403                "fee_data_availability_mode": "L1"
404            }"#,
405        )
406        .unwrap();
407    }
408}