1#![doc = include_str!("../README.md")]
2#![doc(
3 html_logo_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/alloy.jpg",
4 html_favicon_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/favicon.ico"
5)]
6#![cfg_attr(not(test), warn(unused_crate_dependencies))]
7#![cfg_attr(docsrs, feature(doc_cfg))]
8
9use std::str::FromStr;
10
11use alloy_consensus::TxType;
12use alloy_dyn_abi::{DynSolType, DynSolValue};
13use alloy_eips::BlockNumberOrTag;
14use alloy_primitives::{Address, Bloom, Bytes, FixedBytes, Log, I256, U256};
15use serde::{de::Error, Deserialize, Serialize};
16
17#[derive(Debug, Serialize, Deserialize)]
19#[serde(rename_all = "camelCase")]
20pub struct TenderlyEstimateGasResult {
21 #[serde(with = "alloy_serde::quantity")]
23 pub gas: u64,
24 #[serde(with = "alloy_serde::quantity")]
26 pub gas_used: u64,
27}
28
29#[derive(Debug, Serialize, Deserialize)]
31#[serde(rename_all = "camelCase")]
32pub struct TenderlyGasPriceTier {
33 #[serde(with = "alloy_serde::quantity")]
35 pub max_priority_fee_per_gas: u128,
36 #[serde(with = "alloy_serde::quantity")]
38 pub max_fee_per_gas: u128,
39 pub wait_time: u64,
41}
42
43#[derive(Debug, Serialize, Deserialize)]
45#[serde(rename_all = "camelCase")]
46pub struct TenderlyGasPriceResult {
47 #[serde(with = "alloy_serde::quantity")]
49 pub current_block_number: u64,
50 #[serde(with = "alloy_serde::quantity")]
52 pub base_fee_per_gas: u128,
53 pub price: TenderlyGasPriceTiers,
55}
56
57#[derive(Debug, Serialize, Deserialize)]
59#[serde(rename_all = "camelCase")]
60pub struct TenderlyGasPriceTiers {
61 pub low: TenderlyGasPriceTier,
63 pub medium: TenderlyGasPriceTier,
65 pub high: TenderlyGasPriceTier,
67}
68
69#[derive(Clone, Debug, Serialize, Deserialize)]
71#[serde(rename_all = "camelCase")]
72pub struct TenderlyDecodedArgument {
73 #[serde(rename = "value")]
75 raw_value: serde_json::Value,
76 #[serde(rename = "type")]
78 raw_typ: serde_json::Value,
79 pub name: String,
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub indexed: Option<bool>,
84}
85
86impl TenderlyDecodedArgument {
87 pub fn ty(&self) -> Option<DynSolType> {
89 let raw = self.raw_typ.as_str()?;
90 let Ok(ty) = raw.parse() else {
91 return None;
92 };
93 Some(ty)
94 }
95
96 pub fn value(&self) -> Option<DynSolValue> {
98 let Ok(val) = DecodedValue::parse_dyn_value(&self.raw_value, &self.ty()?) else {
99 return None;
100 };
101 Some(val)
102 }
103}
104
105#[derive(Debug, Serialize, Deserialize)]
107#[serde(rename_all = "camelCase")]
108pub struct TenderlyDecodeInputResult {
109 pub name: String,
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub confidence: Option<f64>,
114 pub decoded_arguments: Vec<TenderlyDecodedArgument>,
116}
117
118#[derive(Clone, Debug, Serialize, Deserialize)]
120pub struct TenderlyFunctionInput {
121 #[serde(rename = "type")]
123 raw_typ: serde_json::Value,
124}
125
126impl TenderlyFunctionInput {
127 pub fn ty(&self) -> Option<DynSolType> {
129 let raw = self.raw_typ.as_str()?;
130 raw.parse().ok()
131 }
132}
133
134#[derive(Clone, Debug, Serialize, Deserialize)]
136pub struct TenderlyFunctionSignature {
137 pub name: String,
139 pub inputs: Vec<TenderlyFunctionInput>,
141}
142
143#[derive(Clone, Debug, Serialize, Deserialize)]
145#[serde(rename_all = "camelCase")]
146pub struct TenderlyTransactionRangeParams {
147 #[serde(skip_serializing_if = "Option::is_none")]
149 pub from: Option<Address>,
150 #[serde(skip_serializing_if = "Option::is_none")]
152 pub to: Option<Address>,
153 pub from_block: BlockNumberOrTag,
155 pub to_block: BlockNumberOrTag,
157}
158
159#[derive(Clone, Debug, Serialize, Deserialize)]
161#[serde(rename_all = "camelCase")]
162pub struct TenderlyStorageQueryParams {
163 pub address: Address,
165 pub offset: U256,
167}
168
169#[derive(Clone, Debug, Serialize, Deserialize)]
171#[serde(rename_all = "camelCase")]
172pub struct TenderlyStorageChange {
173 #[serde(with = "alloy_serde::quantity")]
175 pub block_number: u64,
176 pub value: FixedBytes<32>,
178}
179
180#[derive(Debug, Serialize, Deserialize)]
182#[serde(rename_all = "camelCase")]
183pub struct TenderlySimulationResult {
184 pub status: bool,
186 #[serde(with = "alloy_serde::quantity")]
188 pub gas_used: u64,
189 #[serde(with = "alloy_serde::quantity")]
191 pub cumulative_gas_used: u64,
192 pub block_number: BlockNumberOrTag,
194 #[serde(rename = "type")]
196 pub typ: TxType,
197 pub logs_bloom: Bloom,
199 pub logs: Vec<TenderlyLog>,
201 pub trace: Vec<TenderlyTrace>,
203 #[serde(skip_serializing_if = "Option::is_none")]
205 pub asset_changes: Option<Vec<AssetChange>>,
206 #[serde(skip_serializing_if = "Option::is_none")]
208 pub balance_changes: Option<Vec<BalanceChange>>,
209 #[serde(skip_serializing_if = "Option::is_none")]
211 pub state_changes: Option<Vec<StateChange>>,
212}
213
214#[derive(Clone, Serialize, Deserialize, Debug)]
216#[serde(rename_all = "camelCase")]
217pub struct TenderlyLog {
218 pub name: String,
220 pub anonymous: bool,
222 pub inputs: Option<Vec<DecodedValue>>,
225 pub raw: Log,
227}
228
229#[derive(Clone, Debug, Serialize, Deserialize)]
231pub struct DecodedValue {
232 #[serde(rename = "value")]
234 raw_value: serde_json::Value,
235 #[serde(rename = "type")]
237 raw_typ: serde_json::Value,
238 #[serde(skip_serializing_if = "Option::is_none")]
240 pub name: Option<String>,
241 #[serde(skip_serializing_if = "Option::is_none")]
243 pub indexed: Option<bool>,
244}
245
246impl DecodedValue {
247 pub fn ty(&self) -> Option<DynSolType> {
249 let raw = self.raw_typ.as_str()?;
250 let Ok(ty) = raw.parse() else {
251 return None;
252 };
253 Some(ty)
254 }
255
256 #[deprecated = "Use `Self::ty` instead"]
258 pub fn typ(&self) -> Option<DynSolType> {
259 self.ty()
260 }
261
262 pub fn value(&self) -> Option<DynSolValue> {
264 let Ok(val) = Self::parse_dyn_value(&self.raw_value, &self.ty()?) else {
265 return None;
266 };
267 Some(val)
268 }
269
270 pub(crate) fn parse_dyn_value(
272 val: &serde_json::Value,
273 ty: &DynSolType,
274 ) -> Result<DynSolValue, serde_json::error::Error> {
275 use serde_json::Error;
276
277 match ty {
278 DynSolType::Bool => {
279 val.as_bool().map(DynSolValue::Bool).ok_or_else(|| Error::custom("expected bool"))
280 }
281 DynSolType::Uint(bits) => val
282 .as_str()
283 .ok_or_else(|| Error::custom("expected string"))
284 .and_then(|a| U256::from_str(a).map_err(Error::custom))
285 .map(|u| DynSolValue::Uint(u, *bits)),
286 DynSolType::Int(bits) => val
287 .as_str()
288 .ok_or_else(|| Error::custom("expected string"))
289 .and_then(|a| I256::from_str(a).map_err(Error::custom))
290 .map(|i| DynSolValue::Int(i, *bits)),
291 DynSolType::Address => val
292 .as_str()
293 .ok_or_else(|| Error::custom("expected string"))
294 .and_then(|a| Address::from_str(a).map_err(Error::custom))
295 .map(DynSolValue::Address),
296 DynSolType::FixedBytes(size) => val
297 .as_str()
298 .ok_or_else(|| Error::custom("expected string"))
299 .and_then(|a| FixedBytes::from_str(a).map_err(Error::custom))
300 .map(|b| DynSolValue::FixedBytes(b, *size)),
301 DynSolType::Bytes => val
302 .as_str()
303 .ok_or_else(|| Error::custom("expected string"))
304 .and_then(|b| Bytes::from_str(b).map_err(Error::custom))
305 .map(|b| DynSolValue::Bytes(b.into())),
306 DynSolType::String => val
307 .as_str()
308 .ok_or_else(|| Error::custom("expected string"))
309 .map(|s| DynSolValue::String(s.to_owned())),
310 DynSolType::Array(inner) => {
311 let arr = val.as_array().ok_or_else(|| Error::custom("expected array"))?;
312 let values: Vec<DynSolValue> = arr
313 .iter()
314 .map(|v| Self::parse_dyn_value(v, inner))
315 .collect::<Result<Vec<_>, _>>()?;
316 Ok(DynSolValue::Array(values))
317 }
318 DynSolType::FixedArray(inner, size) => {
319 let arr = val.as_array().ok_or_else(|| Error::custom("expected array"))?;
320 if arr.len() != *size {
321 return Err(Error::custom("array size mismatch"));
322 }
323 let values: Vec<DynSolValue> = arr
324 .iter()
325 .map(|v| Self::parse_dyn_value(v, inner))
326 .collect::<Result<Vec<_>, _>>()?;
327 Ok(DynSolValue::FixedArray(values))
328 }
329 DynSolType::Tuple(types) => {
330 let arr = val.as_array().ok_or_else(|| Error::custom("expected tuple"))?;
331 if arr.len() != types.len() {
332 return Err(Error::custom("tuple length mismatch"));
333 }
334 let values = arr
335 .iter()
336 .zip(types.iter())
337 .map(|(v, t)| Self::parse_dyn_value(v, t))
338 .collect::<Result<Vec<_>, _>>()?;
339 Ok(DynSolValue::Tuple(values))
340 }
341 _ => Err(Error::custom("type is not supported")),
342 }
343 }
344}
345
346#[derive(Clone, Serialize, Deserialize, Debug)]
348#[serde(rename_all = "camelCase")]
349pub struct TenderlyTrace {
350 pub r#type: TenderlyCallType,
352 pub from: Address,
354 pub to: Address,
356 #[serde(with = "alloy_serde::quantity")]
358 pub gas: u64,
359 #[serde(with = "alloy_serde::quantity")]
361 pub gas_used: u64,
362 #[serde(skip_serializing_if = "Option::is_none")]
364 pub value: Option<U256>,
365 #[serde(skip_serializing_if = "Option::is_none")]
367 pub error: Option<String>,
368 pub input: Bytes,
370 #[serde(skip_serializing_if = "Option::is_none")]
372 pub decoded_input: Option<Vec<DecodedValue>>,
373 #[serde(skip_serializing_if = "Option::is_none")]
375 pub method: Option<String>,
376 pub output: Bytes,
378 #[serde(skip_serializing_if = "Option::is_none")]
380 pub decoded_output: Option<Vec<DecodedValue>>,
381 pub subtraces: usize,
383 pub trace_address: Vec<usize>,
388}
389
390#[derive(Clone, Serialize, Deserialize, Debug)]
392#[serde(rename_all = "UPPERCASE")]
393pub enum TenderlyCallType {
394 Call,
396 CallCode,
398 StaticCall,
400 DelegateCall,
402 AuthCall,
404}
405
406#[derive(Clone, Serialize, Deserialize, Debug)]
408#[serde(rename_all = "camelCase")]
409pub struct AssetChange {
410 pub asset_info: AssetInfo,
412 pub r#type: ChangeType,
414 #[serde(skip_serializing_if = "Option::is_none")]
416 pub from: Option<Address>,
417 #[serde(skip_serializing_if = "Option::is_none")]
419 pub to: Option<Address>,
420 pub raw_amount: U256,
422 #[serde(skip_serializing_if = "Option::is_none")]
424 pub amount: Option<String>,
425 #[serde(skip_serializing_if = "Option::is_none")]
427 pub dollar_value: Option<String>,
428}
429
430#[derive(Clone, Serialize, Deserialize, Debug)]
432#[serde(rename_all = "camelCase")]
433pub struct AssetInfo {
434 pub standard: AssetStandard,
436 #[serde(skip_serializing_if = "Option::is_none")]
438 pub r#type: Option<AssetFungibility>,
439 #[serde(skip_serializing_if = "Option::is_none")]
441 pub contract_address: Option<Address>,
442 #[serde(skip_serializing_if = "Option::is_none")]
444 pub symbol: Option<String>,
445 #[serde(skip_serializing_if = "Option::is_none")]
447 pub name: Option<String>,
448 #[serde(skip_serializing_if = "Option::is_none")]
450 pub logo: Option<String>,
451 #[serde(skip_serializing_if = "Option::is_none")]
453 pub decimals: Option<u8>,
454 #[serde(skip_serializing_if = "Option::is_none")]
456 pub dollar_value: Option<String>,
457}
458
459#[derive(Clone, Serialize, Deserialize, Debug)]
461#[serde(rename_all = "UPPERCASE")]
462pub enum AssetStandard {
463 #[serde(rename = "NativeCurrency")]
465 NativeCurrency,
466 Erc20,
468 Erc721,
470 Erc1155,
472}
473
474#[derive(Clone, Serialize, Deserialize, Debug)]
476#[serde(rename_all = "PascalCase")]
477pub enum AssetFungibility {
478 Native,
480 Fungible,
482 NonFungible,
484}
485
486#[derive(Clone, Serialize, Deserialize, Debug)]
488#[serde(rename_all = "PascalCase")]
489pub enum ChangeType {
490 Mint,
492 Burn,
494 Transfer,
496}
497
498#[derive(Clone, Serialize, Deserialize, Debug)]
500#[serde(rename_all = "camelCase")]
501pub struct BalanceChange {
502 pub address: Address,
504 pub dollar_value: String,
506 #[serde(skip_serializing_if = "Option::is_none")]
508 pub transfers: Option<Vec<usize>>,
509}
510
511#[derive(Clone, Serialize, Deserialize, Debug)]
513#[serde(rename_all = "camelCase")]
514pub struct StateChange {
515 pub address: Address,
517 #[serde(skip_serializing_if = "Option::is_none")]
519 pub nonce: Option<ValueChange>,
520 #[serde(skip_serializing_if = "Option::is_none")]
522 pub balance: Option<ValueChange>,
523 #[serde(skip_serializing_if = "Option::is_none")]
525 pub storage: Option<Vec<StorageSlotChange>>,
526}
527
528#[derive(Clone, Serialize, Deserialize, Debug)]
530#[serde(rename_all = "camelCase")]
531pub struct StorageSlotChange {
532 pub slot: FixedBytes<32>,
534 pub previous_value: FixedBytes<32>,
536 pub new_value: FixedBytes<32>,
538}
539
540#[derive(Clone, Serialize, Deserialize, Debug)]
542#[serde(rename_all = "camelCase")]
543pub struct ValueChange {
544 pub previous_value: U256,
546 pub new_value: U256,
548}
549
550#[cfg(test)]
551mod tests {
552 use alloy_dyn_abi::{DynSolType, DynSolValue};
553
554 use crate::{
555 TenderlyDecodeInputResult, TenderlyEstimateGasResult, TenderlyGasPriceResult,
556 TenderlySimulationResult,
557 };
558
559 #[test]
560 fn test_success_response() {
561 let input = include_str!("../test_data/success.json");
562 let parsed: TenderlySimulationResult = serde_json::from_str(input).unwrap();
563
564 assert_eq!(
566 serde_json::to_string(&parsed).unwrap().split_whitespace().collect::<String>(),
567 input.split_whitespace().collect::<String>()
568 );
569 }
570
571 #[test]
572 fn test_failure_response() {
573 let input = include_str!("../test_data/failure.json");
574 let parsed: TenderlySimulationResult = serde_json::from_str(input).unwrap();
575
576 assert_eq!(
577 serde_json::to_string(&parsed).unwrap().split_whitespace().collect::<String>(),
578 input.split_whitespace().collect::<String>()
579 );
580 }
581
582 #[test]
583 fn test_bundle_success_response() {
584 let input = include_str!("../test_data/bundle_success.json");
585 let parsed: Vec<TenderlySimulationResult> = serde_json::from_str(input).unwrap();
586
587 assert_eq!(
588 serde_json::to_string(&parsed).unwrap().split_whitespace().collect::<String>(),
589 input.split_whitespace().collect::<String>()
590 );
591 }
592
593 #[test]
594 fn test_trace_success_response() {
595 let input = include_str!("../test_data/trace_success.json");
596 let parsed: TenderlySimulationResult = serde_json::from_str(input).unwrap();
597
598 assert_eq!(
599 serde_json::to_string(&parsed).unwrap().split_whitespace().collect::<String>(),
600 input.split_whitespace().collect::<String>()
601 );
602 }
603
604 #[test]
605 fn test_trace_complex_response() {
606 let input = include_str!("../test_data/trace_complex.json");
607 let parsed: TenderlySimulationResult = serde_json::from_str(input).unwrap();
608
609 assert_eq!(
610 serde_json::to_string(&parsed).unwrap().split_whitespace().collect::<String>(),
611 input.split_whitespace().collect::<String>()
612 );
613 }
614
615 #[test]
616 fn test_trace_swap_response() {
617 let input = include_str!("../test_data/trace_swap.json");
618 let parsed: TenderlySimulationResult = serde_json::from_str(input).unwrap();
619
620 assert_eq!(
621 serde_json::to_string(&parsed).unwrap().split_whitespace().collect::<String>(),
622 input.split_whitespace().collect::<String>()
623 );
624 }
625
626 #[test]
627 fn test_estimate_gas_response() {
628 let input = include_str!("../test_data/estimate_gas.json");
629 let parsed: TenderlyEstimateGasResult = serde_json::from_str(input).unwrap();
630
631 assert_eq!(parsed.gas, 0x12579);
632 assert_eq!(parsed.gas_used, 0xff06);
633
634 assert_eq!(
635 serde_json::to_string(&parsed).unwrap().split_whitespace().collect::<String>(),
636 input.split_whitespace().collect::<String>()
637 );
638 }
639
640 #[test]
641 fn test_gas_price_response() {
642 let input = include_str!("../test_data/gas_price.json");
643 let parsed: TenderlyGasPriceResult = serde_json::from_str(input).unwrap();
644
645 assert_eq!(parsed.current_block_number, 0x14a0bdb);
646 assert_eq!(parsed.base_fee_per_gas, 0xbcdd3e0f);
647 assert_eq!(parsed.price.low.max_priority_fee_per_gas, 0x27f840a);
648 assert_eq!(parsed.price.low.max_fee_per_gas, 0x1097c5d8b);
649 assert_eq!(parsed.price.low.wait_time, 36000);
650 assert_eq!(parsed.price.medium.max_priority_fee_per_gas, 0x9b4c5bb);
651 assert_eq!(parsed.price.medium.max_fee_per_gas, 0x1137c6003);
652 assert_eq!(parsed.price.medium.wait_time, 24000);
653 assert_eq!(parsed.price.high.max_priority_fee_per_gas, 0x10128f8e);
654 assert_eq!(parsed.price.high.max_fee_per_gas, 0x11c5174a8);
655 assert_eq!(parsed.price.high.wait_time, 12000);
656
657 assert_eq!(
658 serde_json::to_string(&parsed).unwrap().split_whitespace().collect::<String>(),
659 input.split_whitespace().collect::<String>()
660 );
661 }
662
663 #[test]
664 fn test_estimate_gas_bundle_response() {
665 let input = include_str!("../test_data/estimate_gas_bundle.json");
666 let parsed: Vec<TenderlyEstimateGasResult> = serde_json::from_str(input).unwrap();
667
668 assert_eq!(parsed.len(), 4);
669 assert_eq!(parsed[0].gas, 0x12579);
670 assert_eq!(parsed[0].gas_used, 0xff06);
671 assert_eq!(parsed[1].gas, 0x10918);
672 assert_eq!(parsed[1].gas_used, 0xb551);
673 assert_eq!(parsed[2].gas, 0xa621);
674 assert_eq!(parsed[2].gas_used, 0x6625);
675 assert_eq!(parsed[3].gas, 0x10649);
676 assert_eq!(parsed[3].gas_used, 0xb249);
677
678 assert_eq!(
679 serde_json::to_string(&parsed).unwrap().split_whitespace().collect::<String>(),
680 input.split_whitespace().collect::<String>()
681 );
682 }
683
684 #[test]
685 fn test_decode_input_response() {
686 let input = include_str!("../test_data/decode_input.json");
687 let parsed: TenderlyDecodeInputResult = serde_json::from_str(input).unwrap();
688
689 assert_eq!(parsed.name, "transfer");
690 assert_eq!(parsed.decoded_arguments.len(), 2);
691
692 let arg0 = &parsed.decoded_arguments[0];
694 assert_eq!(arg0.name, "arg0");
695 let ty0 = arg0.ty().expect("should parse address type");
696 assert!(matches!(ty0, DynSolType::Address));
697 let value0 = arg0.value().expect("should parse address value");
698 assert!(matches!(value0, DynSolValue::Address(_)));
699
700 let arg1 = &parsed.decoded_arguments[1];
702 assert_eq!(arg1.name, "arg1");
703 let ty1 = arg1.ty().expect("should parse uint256 type");
704 assert!(matches!(ty1, DynSolType::Uint(_)));
705 let value1 = arg1.value().expect("should parse uint256 value");
706 assert!(matches!(value1, DynSolValue::Uint(_, _)));
707
708 let serialized = serde_json::to_string(&parsed).unwrap();
710 let reparsed: TenderlyDecodeInputResult = serde_json::from_str(&serialized).unwrap();
711 assert_eq!(reparsed.name, parsed.name);
712 assert_eq!(reparsed.decoded_arguments.len(), parsed.decoded_arguments.len());
713 }
714
715 #[test]
716 fn test_decode_error_response() {
717 let input = include_str!("../test_data/decode_error.json");
718 let parsed: TenderlyDecodeInputResult = serde_json::from_str(input).unwrap();
719
720 assert_eq!(parsed.name, "ERC20InsufficientBalance");
721 assert_eq!(parsed.decoded_arguments.len(), 3);
722
723 let arg0 = &parsed.decoded_arguments[0];
725 assert_eq!(arg0.name, "arg0");
726 let ty0 = arg0.ty().expect("should parse address type");
727 assert!(matches!(ty0, DynSolType::Address));
728 let value0 = arg0.value().expect("should parse address value");
729 assert!(matches!(value0, DynSolValue::Address(_)));
730
731 let arg1 = &parsed.decoded_arguments[1];
733 assert_eq!(arg1.name, "arg1");
734 let ty1 = arg1.ty().expect("should parse uint256 type");
735 assert!(matches!(ty1, DynSolType::Uint(_)));
736 let value1 = arg1.value().expect("should parse uint256 value");
737 assert!(matches!(value1, DynSolValue::Uint(_, _)));
738
739 let arg2 = &parsed.decoded_arguments[2];
741 assert_eq!(arg2.name, "arg2");
742 let ty2 = arg2.ty().expect("should parse uint256 type");
743 assert!(matches!(ty2, DynSolType::Uint(_)));
744 let value2 = arg2.value().expect("should parse uint256 value");
745 assert!(matches!(value2, DynSolValue::Uint(_, _)));
746
747 let serialized = serde_json::to_string(&parsed).unwrap();
749 let reparsed: TenderlyDecodeInputResult = serde_json::from_str(&serialized).unwrap();
750 assert_eq!(reparsed.name, parsed.name);
751 assert_eq!(reparsed.decoded_arguments.len(), parsed.decoded_arguments.len());
752 }
753}