Skip to main content

movement_sdk/transaction/
simulation.rs

1//! Transaction simulation for pre-flight validation.
2//!
3//! This module provides utilities for simulating transactions before submission
4//! to predict outcomes, estimate gas costs, and catch errors early.
5//!
6//! # Overview
7//!
8//! Transaction simulation allows you to:
9//! - **Predict success/failure** before spending gas
10//! - **Estimate gas costs** more accurately than the gas estimator
11//! - **Debug transactions** by examining execution details
12//! - **Validate payloads** before committing to transactions
13//!
14//! # Example
15//!
16//! ```rust,ignore
17//! use movement_sdk::transaction::simulation::SimulationResult;
18//!
19//! let movement = Movement::testnet()?;
20//!
21//! // Simulate a transaction
22//! let result = movement.simulate_payload(&account, payload).await?;
23//!
24//! if result.success() {
25//!     println!("Transaction will succeed!");
26//!     println!("Estimated gas: {}", result.gas_used());
27//! } else {
28//!     println!("Transaction will fail: {}", result.vm_status());
29//! }
30//! ```
31
32use crate::error::{MovementError, MovementResult};
33use serde::{Deserialize, Serialize};
34
35/// Result of a transaction simulation.
36///
37/// Contains detailed information about what would happen if the transaction
38/// were submitted to the network.
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct SimulationResult {
41    /// Whether the transaction would succeed.
42    success: bool,
43    /// The VM status (explains failure if not successful).
44    vm_status: String,
45    /// Gas used by the transaction.
46    gas_used: u64,
47    /// Maximum gas amount specified.
48    max_gas_amount: u64,
49    /// Gas unit price.
50    gas_unit_price: u64,
51    /// Changes that would be made to the state.
52    changes: Vec<StateChange>,
53    /// Events that would be emitted.
54    events: Vec<SimulatedEvent>,
55    /// The transaction hash (would be this if submitted).
56    hash: String,
57    /// Detailed VM error information (if failed).
58    vm_error: Option<VmError>,
59    /// Raw response data for advanced use.
60    raw: serde_json::Value,
61}
62
63impl SimulationResult {
64    /// Parses a simulation result from the API response.
65    ///
66    /// # Errors
67    ///
68    /// Returns an error if the response is empty or if parsing the JSON fails.
69    pub fn from_response(response: Vec<serde_json::Value>) -> MovementResult<Self> {
70        let data = response
71            .into_iter()
72            .next()
73            .ok_or_else(|| MovementError::Api {
74                status_code: 200,
75                message: "Empty simulation response".into(),
76                error_code: None,
77                vm_error_code: None,
78            })?;
79
80        Self::from_json(data)
81    }
82
83    /// Parses a simulation result from JSON.
84    ///
85    /// # Errors
86    ///
87    /// Returns an error if the JSON structure is invalid or missing required fields.
88    pub fn from_json(data: serde_json::Value) -> MovementResult<Self> {
89        let success = data
90            .get("success")
91            .and_then(serde_json::Value::as_bool)
92            .unwrap_or(false);
93
94        let vm_status = data
95            .get("vm_status")
96            .and_then(serde_json::Value::as_str)
97            .unwrap_or("Unknown")
98            .to_string();
99
100        let gas_used = data
101            .get("gas_used")
102            .and_then(serde_json::Value::as_str)
103            .and_then(|s| s.parse().ok())
104            .unwrap_or(0);
105
106        let max_gas_amount = data
107            .get("max_gas_amount")
108            .and_then(serde_json::Value::as_str)
109            .and_then(|s| s.parse().ok())
110            .unwrap_or(0);
111
112        let gas_unit_price = data
113            .get("gas_unit_price")
114            .and_then(serde_json::Value::as_str)
115            .and_then(|s| s.parse().ok())
116            .unwrap_or(0);
117
118        let hash = data
119            .get("hash")
120            .and_then(serde_json::Value::as_str)
121            .unwrap_or("")
122            .to_string();
123
124        // Parse state changes
125        let changes = data
126            .get("changes")
127            .and_then(serde_json::Value::as_array)
128            .map(|arr| arr.iter().map(StateChange::from_json).collect())
129            .unwrap_or_default();
130
131        // Parse events
132        let events = data
133            .get("events")
134            .and_then(|v| v.as_array())
135            .map(|arr| arr.iter().map(SimulatedEvent::from_json).collect())
136            .unwrap_or_default();
137
138        // Parse VM error if present
139        let vm_error = if success {
140            None
141        } else {
142            Some(VmError::from_status(&vm_status))
143        };
144
145        Ok(Self {
146            success,
147            vm_status,
148            gas_used,
149            max_gas_amount,
150            gas_unit_price,
151            changes,
152            events,
153            hash,
154            vm_error,
155            raw: data,
156        })
157    }
158
159    /// Returns whether the transaction would succeed.
160    pub fn success(&self) -> bool {
161        self.success
162    }
163
164    /// Returns whether the transaction would fail.
165    pub fn failed(&self) -> bool {
166        !self.success
167    }
168
169    /// Returns the VM status message.
170    pub fn vm_status(&self) -> &str {
171        &self.vm_status
172    }
173
174    /// Returns the gas that would be used.
175    pub fn gas_used(&self) -> u64 {
176        self.gas_used
177    }
178
179    /// Returns the maximum gas amount specified.
180    pub fn max_gas_amount(&self) -> u64 {
181        self.max_gas_amount
182    }
183
184    /// Returns the gas unit price.
185    pub fn gas_unit_price(&self) -> u64 {
186        self.gas_unit_price
187    }
188
189    /// Returns the total gas cost in octas.
190    pub fn gas_cost(&self) -> u64 {
191        self.gas_used.saturating_mul(self.gas_unit_price)
192    }
193
194    /// Returns the estimated gas cost with a safety margin.
195    ///
196    /// Adds 20% to the simulated gas to account for variations.
197    pub fn safe_gas_estimate(&self) -> u64 {
198        self.gas_used.saturating_mul(120) / 100
199    }
200
201    /// Returns the state changes that would be made.
202    pub fn changes(&self) -> &[StateChange] {
203        &self.changes
204    }
205
206    /// Returns the events that would be emitted.
207    pub fn events(&self) -> &[SimulatedEvent] {
208        &self.events
209    }
210
211    /// Returns the transaction hash (would be this if submitted).
212    pub fn hash(&self) -> &str {
213        &self.hash
214    }
215
216    /// Returns detailed VM error information if the simulation failed.
217    pub fn vm_error(&self) -> Option<&VmError> {
218        self.vm_error.as_ref()
219    }
220
221    /// Returns the raw JSON response for advanced use.
222    pub fn raw(&self) -> &serde_json::Value {
223        &self.raw
224    }
225
226    /// Checks if the failure is due to insufficient balance.
227    pub fn is_insufficient_balance(&self) -> bool {
228        self.vm_error
229            .as_ref()
230            .is_some_and(VmError::is_insufficient_balance)
231    }
232
233    /// Checks if the failure is due to sequence number issues.
234    pub fn is_sequence_number_error(&self) -> bool {
235        self.vm_error
236            .as_ref()
237            .is_some_and(VmError::is_sequence_number_error)
238    }
239
240    /// Checks if the failure is due to out of gas.
241    pub fn is_out_of_gas(&self) -> bool {
242        self.vm_error.as_ref().is_some_and(VmError::is_out_of_gas)
243    }
244
245    /// Returns a user-friendly error message if the simulation failed.
246    pub fn error_message(&self) -> Option<String> {
247        if self.success {
248            return None;
249        }
250
251        self.vm_error
252            .as_ref()
253            .map(VmError::user_message)
254            .or_else(|| Some(self.vm_status.clone()))
255    }
256}
257
258/// A state change from a simulated transaction.
259#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct StateChange {
261    /// Type of change (`write_resource`, `delete_resource`, etc.)
262    pub change_type: String,
263    /// Address affected.
264    pub address: String,
265    /// Resource type (for resource changes).
266    pub resource_type: Option<String>,
267    /// Module name (for module changes).
268    pub module: Option<String>,
269    /// The new data (for writes).
270    pub data: Option<serde_json::Value>,
271}
272
273impl StateChange {
274    fn from_json(json: &serde_json::Value) -> Self {
275        Self {
276            change_type: json
277                .get("type")
278                .and_then(serde_json::Value::as_str)
279                .unwrap_or("unknown")
280                .to_string(),
281            address: json
282                .get("address")
283                .and_then(serde_json::Value::as_str)
284                .unwrap_or("")
285                .to_string(),
286            resource_type: json
287                .get("data")
288                .and_then(|d| d.get("type"))
289                .and_then(serde_json::Value::as_str)
290                .map(ToString::to_string),
291            module: json
292                .get("module")
293                .and_then(serde_json::Value::as_str)
294                .map(ToString::to_string),
295            data: json.get("data").cloned(),
296        }
297    }
298
299    /// Returns true if this is a resource write.
300    pub fn is_write(&self) -> bool {
301        self.change_type == "write_resource"
302    }
303
304    /// Returns true if this is a resource delete.
305    pub fn is_delete(&self) -> bool {
306        self.change_type == "delete_resource"
307    }
308}
309
310/// An event from a simulated transaction.
311#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct SimulatedEvent {
313    /// The event type.
314    pub event_type: String,
315    /// Sequence number of the event.
316    pub sequence_number: u64,
317    /// Event data.
318    pub data: serde_json::Value,
319}
320
321impl SimulatedEvent {
322    fn from_json(json: &serde_json::Value) -> Self {
323        Self {
324            event_type: json
325                .get("type")
326                .and_then(|v| v.as_str())
327                .unwrap_or("")
328                .to_string(),
329            sequence_number: json
330                .get("sequence_number")
331                .and_then(|v| v.as_str())
332                .and_then(|s| s.parse().ok())
333                .unwrap_or(0),
334            data: json.get("data").cloned().unwrap_or(serde_json::Value::Null),
335        }
336    }
337}
338
339/// Detailed VM error information.
340#[derive(Debug, Clone, Serialize, Deserialize)]
341pub struct VmError {
342    /// Error category.
343    pub category: VmErrorCategory,
344    /// The raw VM status string.
345    pub status: String,
346    /// Abort code (if applicable).
347    pub abort_code: Option<u64>,
348    /// Location of the error (`module::function`).
349    pub location: Option<String>,
350}
351
352impl VmError {
353    fn from_status(status: &str) -> Self {
354        let category = VmErrorCategory::from_status(status);
355
356        // Try to extract abort code
357        let abort_code = if status.contains("ABORTED") {
358            // Parse abort code from status like "Move abort in 0x1::coin: EINSUFFICIENT_BALANCE(0x10001)"
359            status
360                .split('(')
361                .nth(1)
362                .and_then(|s| s.trim_end_matches(')').parse().ok())
363        } else {
364            None
365        };
366
367        // Try to extract location
368        let location = if status.contains("::") {
369            status
370                .split_whitespace()
371                .find(|s| s.contains("::"))
372                .map(|s| s.trim_end_matches(':').to_string())
373        } else {
374            None
375        };
376
377        Self {
378            category,
379            status: status.to_string(),
380            abort_code,
381            location,
382        }
383    }
384
385    /// Returns true if this is an insufficient balance error.
386    pub fn is_insufficient_balance(&self) -> bool {
387        matches!(self.category, VmErrorCategory::InsufficientBalance)
388            || self.status.contains("INSUFFICIENT")
389            || self.status.contains("NOT_ENOUGH")
390    }
391
392    /// Returns true if this is a sequence number error.
393    pub fn is_sequence_number_error(&self) -> bool {
394        matches!(self.category, VmErrorCategory::SequenceNumber)
395    }
396
397    /// Returns true if this is an out of gas error.
398    pub fn is_out_of_gas(&self) -> bool {
399        matches!(self.category, VmErrorCategory::OutOfGas)
400    }
401
402    /// Returns a user-friendly error message.
403    pub fn user_message(&self) -> String {
404        match self.category {
405            VmErrorCategory::InsufficientBalance => {
406                "Insufficient balance to complete this transaction".to_string()
407            }
408            VmErrorCategory::SequenceNumber => {
409                "Transaction sequence number mismatch - the account's sequence number may have changed".to_string()
410            }
411            VmErrorCategory::OutOfGas => {
412                "Transaction ran out of gas - try increasing max_gas_amount".to_string()
413            }
414            VmErrorCategory::MoveAbort => {
415                if let Some(code) = self.abort_code {
416                    format!("Transaction aborted with code {code}")
417                } else {
418                    "Transaction was aborted by the Move VM".to_string()
419                }
420            }
421            VmErrorCategory::ResourceNotFound => {
422                "Required resource not found on chain".to_string()
423            }
424            VmErrorCategory::ModuleNotFound => {
425                "Required module not found on chain".to_string()
426            }
427            VmErrorCategory::FunctionNotFound => {
428                "Function not found in the specified module".to_string()
429            }
430            VmErrorCategory::TypeMismatch => {
431                "Type argument mismatch in function call".to_string()
432            }
433            VmErrorCategory::Unknown => self.status.clone(),
434        }
435    }
436}
437
438/// Categories of VM errors.
439#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
440pub enum VmErrorCategory {
441    /// Insufficient account balance.
442    InsufficientBalance,
443    /// Sequence number mismatch.
444    SequenceNumber,
445    /// Ran out of gas.
446    OutOfGas,
447    /// Move abort (smart contract error).
448    MoveAbort,
449    /// Resource not found.
450    ResourceNotFound,
451    /// Module not found.
452    ModuleNotFound,
453    /// Function not found.
454    FunctionNotFound,
455    /// Type argument mismatch.
456    TypeMismatch,
457    /// Unknown error.
458    Unknown,
459}
460
461impl VmErrorCategory {
462    fn from_status(status: &str) -> Self {
463        let status_upper = status.to_uppercase();
464
465        if status_upper.contains("INSUFFICIENT") || status_upper.contains("NOT_ENOUGH") {
466            Self::InsufficientBalance
467        } else if status_upper.contains("SEQUENCE_NUMBER")
468            || status_upper.contains("SEQUENCE NUMBER")
469        {
470            Self::SequenceNumber
471        } else if status_upper.contains("OUT_OF_GAS") || status_upper.contains("OUT OF GAS") {
472            Self::OutOfGas
473        } else if status_upper.contains("ABORT") {
474            Self::MoveAbort
475        } else if status_upper.contains("RESOURCE") && status_upper.contains("NOT") {
476            Self::ResourceNotFound
477        } else if status_upper.contains("MODULE") && status_upper.contains("NOT") {
478            Self::ModuleNotFound
479        } else if status_upper.contains("FUNCTION") && status_upper.contains("NOT") {
480            Self::FunctionNotFound
481        } else if status_upper.contains("TYPE")
482            && (status_upper.contains("MISMATCH") || status_upper.contains("ERROR"))
483        {
484            Self::TypeMismatch
485        } else {
486            Self::Unknown
487        }
488    }
489}
490
491/// Options for simulation.
492#[derive(Debug, Clone, Default)]
493pub struct SimulationOptions {
494    /// Whether to estimate gas only (faster).
495    pub estimate_gas_only: bool,
496    /// Override the sender's sequence number.
497    pub sequence_number_override: Option<u64>,
498    /// Override the gas unit price.
499    pub gas_unit_price_override: Option<u64>,
500    /// Override the max gas amount.
501    pub max_gas_amount_override: Option<u64>,
502}
503
504impl SimulationOptions {
505    /// Creates new simulation options.
506    #[must_use]
507    pub fn new() -> Self {
508        Self::default()
509    }
510
511    /// Sets gas-only estimation mode.
512    #[must_use]
513    pub fn estimate_gas_only(mut self) -> Self {
514        self.estimate_gas_only = true;
515        self
516    }
517
518    /// Overrides the sequence number.
519    #[must_use]
520    pub fn with_sequence_number(mut self, seq: u64) -> Self {
521        self.sequence_number_override = Some(seq);
522        self
523    }
524
525    /// Overrides the gas unit price.
526    #[must_use]
527    pub fn with_gas_unit_price(mut self, price: u64) -> Self {
528        self.gas_unit_price_override = Some(price);
529        self
530    }
531
532    /// Overrides the max gas amount.
533    #[must_use]
534    pub fn with_max_gas_amount(mut self, amount: u64) -> Self {
535        self.max_gas_amount_override = Some(amount);
536        self
537    }
538}
539
540#[cfg(test)]
541mod tests {
542    use super::*;
543
544    #[test]
545    fn test_parse_success_result() {
546        let json = serde_json::json!({
547            "success": true,
548            "vm_status": "Executed successfully",
549            "gas_used": "100",
550            "max_gas_amount": "200000",
551            "gas_unit_price": "100",
552            "hash": "0x123",
553            "changes": [],
554            "events": []
555        });
556
557        let result = SimulationResult::from_json(json).unwrap();
558        assert!(result.success());
559        assert_eq!(result.gas_used(), 100);
560        assert_eq!(result.gas_cost(), 10000);
561    }
562
563    #[test]
564    fn test_parse_failed_result() {
565        let json = serde_json::json!({
566            "success": false,
567            "vm_status": "Move abort in 0x1::coin: EINSUFFICIENT_BALANCE(0x10001)",
568            "gas_used": "50",
569            "max_gas_amount": "200000",
570            "gas_unit_price": "100",
571            "hash": "0x456",
572            "changes": [],
573            "events": []
574        });
575
576        let result = SimulationResult::from_json(json).unwrap();
577        assert!(result.failed());
578        assert!(result.is_insufficient_balance());
579        assert!(result.vm_error().is_some());
580    }
581
582    #[test]
583    fn test_error_categories() {
584        assert_eq!(
585            VmErrorCategory::from_status("INSUFFICIENT_BALANCE"),
586            VmErrorCategory::InsufficientBalance
587        );
588        assert_eq!(
589            VmErrorCategory::from_status("SEQUENCE_NUMBER_TOO_OLD"),
590            VmErrorCategory::SequenceNumber
591        );
592        assert_eq!(
593            VmErrorCategory::from_status("OUT_OF_GAS"),
594            VmErrorCategory::OutOfGas
595        );
596        assert_eq!(
597            VmErrorCategory::from_status("Move abort"),
598            VmErrorCategory::MoveAbort
599        );
600    }
601
602    #[test]
603    fn test_safe_gas_estimate() {
604        let json = serde_json::json!({
605            "success": true,
606            "vm_status": "Executed successfully",
607            "gas_used": "1000",
608            "max_gas_amount": "200000",
609            "gas_unit_price": "100",
610            "hash": "0x123",
611            "changes": [],
612            "events": []
613        });
614
615        let result = SimulationResult::from_json(json).unwrap();
616        assert_eq!(result.gas_used(), 1000);
617        assert_eq!(result.safe_gas_estimate(), 1200); // 20% more
618    }
619
620    #[test]
621    fn test_parse_events() {
622        let json = serde_json::json!({
623            "success": true,
624            "vm_status": "Executed successfully",
625            "gas_used": "100",
626            "max_gas_amount": "200000",
627            "gas_unit_price": "100",
628            "hash": "0x123",
629            "changes": [],
630            "events": [
631                {
632                    "type": "0x1::coin::DepositEvent",
633                    "sequence_number": "5",
634                    "data": {"amount": "1000"}
635                }
636            ]
637        });
638
639        let result = SimulationResult::from_json(json).unwrap();
640        assert_eq!(result.events().len(), 1);
641        assert_eq!(result.events()[0].event_type, "0x1::coin::DepositEvent");
642        assert_eq!(result.events()[0].sequence_number, 5);
643    }
644
645    #[test]
646    fn test_parse_changes() {
647        let json = serde_json::json!({
648            "success": true,
649            "vm_status": "Executed successfully",
650            "gas_used": "100",
651            "max_gas_amount": "200000",
652            "gas_unit_price": "100",
653            "hash": "0x123",
654            "changes": [
655                {
656                    "type": "write_resource",
657                    "address": "0x1",
658                    "data": {"type": "0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>"}
659                }
660            ],
661            "events": []
662        });
663
664        let result = SimulationResult::from_json(json).unwrap();
665        assert_eq!(result.changes().len(), 1);
666        assert!(result.changes()[0].is_write());
667    }
668
669    #[test]
670    fn test_simulation_options_default() {
671        let opts = SimulationOptions::default();
672        assert!(!opts.estimate_gas_only);
673        assert!(opts.sequence_number_override.is_none());
674        assert!(opts.gas_unit_price_override.is_none());
675        assert!(opts.max_gas_amount_override.is_none());
676    }
677
678    #[test]
679    fn test_simulation_options_builder() {
680        let opts = SimulationOptions::new()
681            .estimate_gas_only()
682            .with_sequence_number(5)
683            .with_gas_unit_price(200)
684            .with_max_gas_amount(500_000);
685
686        assert!(opts.estimate_gas_only);
687        assert_eq!(opts.sequence_number_override, Some(5));
688        assert_eq!(opts.gas_unit_price_override, Some(200));
689        assert_eq!(opts.max_gas_amount_override, Some(500_000));
690    }
691
692    #[test]
693    fn test_vm_error_category_resource_not_found() {
694        assert_eq!(
695            VmErrorCategory::from_status("RESOURCE_NOT_FOUND"),
696            VmErrorCategory::ResourceNotFound
697        );
698    }
699
700    #[test]
701    fn test_vm_error_category_module_not_found() {
702        assert_eq!(
703            VmErrorCategory::from_status("MODULE_NOT_PUBLISHED"),
704            VmErrorCategory::ModuleNotFound
705        );
706    }
707
708    #[test]
709    fn test_vm_error_category_function_not_found() {
710        assert_eq!(
711            VmErrorCategory::from_status("FUNCTION_NOT_FOUND"),
712            VmErrorCategory::FunctionNotFound
713        );
714    }
715
716    #[test]
717    fn test_vm_error_category_type_mismatch() {
718        assert_eq!(
719            VmErrorCategory::from_status("TYPE_MISMATCH"),
720            VmErrorCategory::TypeMismatch
721        );
722        assert_eq!(
723            VmErrorCategory::from_status("TYPE_ERROR"),
724            VmErrorCategory::TypeMismatch
725        );
726    }
727
728    #[test]
729    fn test_vm_error_category_unknown() {
730        assert_eq!(
731            VmErrorCategory::from_status("SOME_RANDOM_ERROR"),
732            VmErrorCategory::Unknown
733        );
734    }
735
736    #[test]
737    fn test_simulation_result_accessors() {
738        let json = serde_json::json!({
739            "success": true,
740            "vm_status": "Executed successfully",
741            "gas_used": "1500",
742            "max_gas_amount": "200000",
743            "gas_unit_price": "100",
744            "hash": "0xabc123",
745            "changes": [],
746            "events": []
747        });
748
749        let result = SimulationResult::from_json(json).unwrap();
750        assert!(result.success());
751        assert!(!result.failed());
752        assert_eq!(result.vm_status(), "Executed successfully");
753        assert_eq!(result.gas_used(), 1500);
754        assert_eq!(result.max_gas_amount(), 200_000);
755        assert_eq!(result.gas_unit_price(), 100);
756        assert_eq!(result.gas_cost(), 150_000); // 1500 * 100
757        assert_eq!(result.hash(), "0xabc123");
758        assert!(result.events().is_empty());
759        assert!(result.changes().is_empty());
760    }
761
762    #[test]
763    fn test_simulation_result_from_response() {
764        let response = vec![serde_json::json!({
765            "success": true,
766            "vm_status": "Executed successfully",
767            "gas_used": "100",
768            "max_gas_amount": "200000",
769            "gas_unit_price": "100",
770            "hash": "0x123",
771            "changes": [],
772            "events": []
773        })];
774
775        let result = SimulationResult::from_response(response).unwrap();
776        assert!(result.success());
777    }
778
779    #[test]
780    fn test_simulation_result_from_empty_response() {
781        let response: Vec<serde_json::Value> = vec![];
782        let result = SimulationResult::from_response(response);
783        assert!(result.is_err());
784    }
785
786    #[test]
787    fn test_state_change_delete() {
788        let json = serde_json::json!({
789            "success": true,
790            "vm_status": "Executed successfully",
791            "gas_used": "100",
792            "max_gas_amount": "200000",
793            "gas_unit_price": "100",
794            "hash": "0x123",
795            "changes": [
796                {
797                    "type": "delete_resource",
798                    "address": "0x1",
799                    "data": {}
800                }
801            ],
802            "events": []
803        });
804
805        let result = SimulationResult::from_json(json).unwrap();
806        assert_eq!(result.changes().len(), 1);
807        assert!(result.changes()[0].is_delete());
808        assert!(!result.changes()[0].is_write());
809    }
810
811    #[test]
812    fn test_simulation_result_with_vm_error() {
813        let json = serde_json::json!({
814            "success": false,
815            "vm_status": "INSUFFICIENT_BALANCE",
816            "gas_used": "0",
817            "max_gas_amount": "200000",
818            "gas_unit_price": "100",
819            "hash": "0x123",
820            "changes": [],
821            "events": []
822        });
823
824        let result = SimulationResult::from_json(json).unwrap();
825        assert!(result.failed());
826        assert!(result.is_insufficient_balance());
827        assert!(!result.is_out_of_gas());
828        assert!(!result.is_sequence_number_error());
829    }
830
831    #[test]
832    fn test_simulation_result_out_of_gas() {
833        let json = serde_json::json!({
834            "success": false,
835            "vm_status": "OUT_OF_GAS",
836            "gas_used": "200000",
837            "max_gas_amount": "200000",
838            "gas_unit_price": "100",
839            "hash": "0x123",
840            "changes": [],
841            "events": []
842        });
843
844        let result = SimulationResult::from_json(json).unwrap();
845        assert!(result.is_out_of_gas());
846    }
847
848    #[test]
849    fn test_simulation_result_sequence_error() {
850        let json = serde_json::json!({
851            "success": false,
852            "vm_status": "SEQUENCE_NUMBER_TOO_OLD",
853            "gas_used": "0",
854            "max_gas_amount": "200000",
855            "gas_unit_price": "100",
856            "hash": "0x123",
857            "changes": [],
858            "events": []
859        });
860
861        let result = SimulationResult::from_json(json).unwrap();
862        assert!(result.is_sequence_number_error());
863    }
864
865    #[test]
866    fn test_simulated_event_parsing() {
867        let json = serde_json::json!({
868            "success": true,
869            "vm_status": "Executed successfully",
870            "gas_used": "100",
871            "max_gas_amount": "200000",
872            "gas_unit_price": "100",
873            "hash": "0x123",
874            "changes": [],
875            "events": [
876                {
877                    "type": "0x1::coin::WithdrawEvent",
878                    "sequence_number": "10",
879                    "data": {"amount": "500"}
880                },
881                {
882                    "type": "0x1::coin::DepositEvent",
883                    "sequence_number": "20",
884                    "data": {"amount": "500"}
885                }
886            ]
887        });
888
889        let result = SimulationResult::from_json(json).unwrap();
890        assert_eq!(result.events().len(), 2);
891        assert_eq!(result.events()[0].event_type, "0x1::coin::WithdrawEvent");
892        assert_eq!(result.events()[0].sequence_number, 10);
893        assert_eq!(result.events()[1].event_type, "0x1::coin::DepositEvent");
894        assert_eq!(result.events()[1].sequence_number, 20);
895    }
896
897    #[test]
898    fn test_vm_error_user_messages() {
899        // Test all error category user messages
900        let insufficient = VmError::from_status("INSUFFICIENT_BALANCE");
901        assert!(insufficient.user_message().contains("Insufficient"));
902
903        let seq_error = VmError::from_status("SEQUENCE_NUMBER_TOO_OLD");
904        assert!(seq_error.user_message().contains("sequence number"));
905
906        let out_of_gas = VmError::from_status("OUT_OF_GAS");
907        assert!(out_of_gas.user_message().contains("gas"));
908
909        let resource_not_found = VmError::from_status("RESOURCE_NOT_FOUND");
910        assert!(resource_not_found.user_message().contains("resource"));
911
912        let module_not_found = VmError::from_status("MODULE_NOT_PUBLISHED");
913        assert!(module_not_found.user_message().contains("module"));
914
915        let function_not_found = VmError::from_status("FUNCTION_NOT_FOUND");
916        assert!(function_not_found.user_message().contains("Function"));
917
918        let type_mismatch = VmError::from_status("TYPE_MISMATCH");
919        assert!(type_mismatch.user_message().contains("Type"));
920
921        let unknown = VmError::from_status("UNKNOWN_ERROR_XYZ");
922        assert_eq!(unknown.user_message(), "UNKNOWN_ERROR_XYZ");
923    }
924
925    #[test]
926    fn test_vm_error_move_abort_with_code() {
927        // The status needs to contain "ABORTED" for abort_code parsing
928        let abort = VmError::from_status("ABORTED in 0x1::coin: SOME_ERROR(65537)");
929        assert_eq!(abort.category, VmErrorCategory::MoveAbort);
930        assert_eq!(abort.abort_code, Some(65537));
931        assert!(abort.location.is_some());
932        assert!(abort.user_message().contains("65537"));
933    }
934
935    #[test]
936    fn test_vm_error_move_abort_without_code() {
937        let abort = VmError::from_status("Move abort");
938        assert_eq!(abort.category, VmErrorCategory::MoveAbort);
939        assert!(abort.abort_code.is_none());
940        assert!(abort.user_message().contains("aborted"));
941    }
942
943    #[test]
944    fn test_simulation_result_error_message_success() {
945        let json = serde_json::json!({
946            "success": true,
947            "vm_status": "Executed successfully",
948            "gas_used": "100",
949            "max_gas_amount": "200000",
950            "gas_unit_price": "100",
951            "hash": "0x123",
952            "changes": [],
953            "events": []
954        });
955
956        let result = SimulationResult::from_json(json).unwrap();
957        assert!(result.error_message().is_none());
958    }
959
960    #[test]
961    fn test_simulation_result_error_message_failure() {
962        let json = serde_json::json!({
963            "success": false,
964            "vm_status": "INSUFFICIENT_BALANCE",
965            "gas_used": "0",
966            "max_gas_amount": "200000",
967            "gas_unit_price": "100",
968            "hash": "0x123",
969            "changes": [],
970            "events": []
971        });
972
973        let result = SimulationResult::from_json(json).unwrap();
974        let error_msg = result.error_message().unwrap();
975        assert!(error_msg.contains("Insufficient"));
976    }
977
978    #[test]
979    fn test_simulation_result_raw_accessor() {
980        let json = serde_json::json!({
981            "success": true,
982            "vm_status": "Executed successfully",
983            "gas_used": "100",
984            "max_gas_amount": "200000",
985            "gas_unit_price": "100",
986            "hash": "0x123",
987            "changes": [],
988            "events": [],
989            "extra_field": "extra_value"
990        });
991
992        let result = SimulationResult::from_json(json).unwrap();
993        let raw = result.raw();
994        assert_eq!(
995            raw.get("extra_field").unwrap().as_str(),
996            Some("extra_value")
997        );
998    }
999
1000    #[test]
1001    fn test_state_change_with_module() {
1002        let json = serde_json::json!({
1003            "type": "write_module",
1004            "address": "0x1",
1005            "module": "my_module"
1006        });
1007
1008        let change = StateChange::from_json(&json);
1009        assert_eq!(change.change_type, "write_module");
1010        assert_eq!(change.module, Some("my_module".to_string()));
1011    }
1012
1013    #[test]
1014    fn test_simulated_event_with_null_data() {
1015        let json = serde_json::json!({
1016            "type": "0x1::event::SomeEvent",
1017            "sequence_number": "5"
1018            // No data field
1019        });
1020
1021        let event = SimulatedEvent::from_json(&json);
1022        assert_eq!(event.event_type, "0x1::event::SomeEvent");
1023        assert_eq!(event.sequence_number, 5);
1024        assert!(event.data.is_null());
1025    }
1026
1027    #[test]
1028    fn test_vm_error_not_enough_variant() {
1029        let error = VmError::from_status("NOT_ENOUGH_GAS");
1030        assert!(error.is_insufficient_balance() || error.status.contains("NOT_ENOUGH"));
1031    }
1032
1033    #[test]
1034    fn test_vm_error_category_sequence_number_variant() {
1035        // Test "SEQUENCE NUMBER" with space
1036        assert_eq!(
1037            VmErrorCategory::from_status("SEQUENCE NUMBER INVALID"),
1038            VmErrorCategory::SequenceNumber
1039        );
1040    }
1041
1042    #[test]
1043    fn test_vm_error_category_out_of_gas_with_space() {
1044        assert_eq!(
1045            VmErrorCategory::from_status("OUT OF GAS"),
1046            VmErrorCategory::OutOfGas
1047        );
1048    }
1049
1050    #[test]
1051    fn test_simulation_result_missing_fields() {
1052        // JSON with minimal fields
1053        let json = serde_json::json!({});
1054
1055        let result = SimulationResult::from_json(json).unwrap();
1056        assert!(!result.success());
1057        assert_eq!(result.gas_used(), 0);
1058        assert_eq!(result.vm_status(), "Unknown");
1059    }
1060
1061    #[test]
1062    fn test_simulation_result_vm_error_accessor() {
1063        let json = serde_json::json!({
1064            "success": false,
1065            "vm_status": "ABORT",
1066            "gas_used": "0",
1067            "max_gas_amount": "200000",
1068            "gas_unit_price": "100",
1069            "hash": "0x123",
1070            "changes": [],
1071            "events": []
1072        });
1073
1074        let result = SimulationResult::from_json(json).unwrap();
1075        assert!(result.vm_error().is_some());
1076        let vm_error = result.vm_error().unwrap();
1077        assert_eq!(vm_error.category, VmErrorCategory::MoveAbort);
1078    }
1079
1080    #[test]
1081    fn test_simulation_options_new() {
1082        let opts = SimulationOptions::new();
1083        assert!(!opts.estimate_gas_only);
1084    }
1085}