starknet_devnet_server/api/
write_endpoints.rs1use 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 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 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 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 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 pub async fn load(&self, path: String) -> StrictRpcResult {
139 let events = load_events(self.api.config.dump_on, &path)?;
140 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 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 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 let mut messages_to_l2 = vec![];
170 let mut generated_l2_transactions = vec![];
171 if !is_dry_run {
172 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 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 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 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 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 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 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 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 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 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 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 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 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}