Skip to main content

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