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