fuel_abi_types/abi/
program.rs

1//! Defines a set of serializable types required for the Fuel VM ABI.
2
3use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6
7/// FuelVM ABI representation in JSON, originally specified
8/// [here](https://github.com/FuelLabs/fuel-specs/blob/master/specs/protocol/abi.md).
9///
10/// This type may be used by compilers and related tooling to convert an ABI
11/// representation into native Rust structs and vice-versa.
12#[derive(Default, Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub struct ProgramABI {
15    pub program_type: String,
16    pub spec_version: Version,
17    pub encoding_version: Version,
18    pub concrete_types: Vec<TypeConcreteDeclaration>,
19    pub metadata_types: Vec<TypeMetadataDeclaration>,
20    pub functions: Vec<ABIFunction>,
21    pub logged_types: Option<Vec<LoggedType>>,
22    pub messages_types: Option<Vec<MessageType>>,
23    pub configurables: Option<Vec<Configurable>>,
24    pub error_codes: Option<BTreeMap<u64, ErrorDetails>>,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub panicking_calls: Option<BTreeMap<u64, PanickingCall>>,
27}
28
29#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub struct Version(pub String);
31
32impl From<&str> for Version {
33    fn from(value: &str) -> Self {
34        Version(value.into())
35    }
36}
37
38impl Version {
39    pub fn major(&self) -> Option<&str> {
40        let s = self.0.split('.').next().map(|x| x.trim());
41        match s {
42            Some("") => None,
43            s => s,
44        }
45    }
46
47    pub fn minor(&self) -> Option<&str> {
48        let s = self.0.split('.').nth(1).map(|x| x.trim());
49        match s {
50            Some("") => None,
51            s => s,
52        }
53    }
54
55    /// Returns `true` if the ABI version is `1.1`.
56    ///
57    /// The v1.1 introduced the "errorCodes" field:
58    ///  - each "errorCodes" entry key is equal to a revert code generated by a `panic` expression.
59    pub fn is_v1_1(&self) -> bool {
60        self.0 == "1.1"
61    }
62
63    /// Returns `true` if the ABI version is `1.2`.
64    ///
65    /// The v1.2 introduced the "panickingCalls" field and changed the semantics of
66    /// the key in the "errorCodes" field:
67    ///  - the key in the "errorCodes" field is the `u64` value of the `pppppppp` part of the `1_pppppppp_CCCCCCCCCCC_CCCCCCCCCCC_CCCCCCCCCCC_CCCCCCCCCCC_CCCCCCCCCCC` encoded revert code generated by a `panic` expression.
68    pub fn is_v1_2(&self) -> bool {
69        self.0 == "1.2"
70    }
71}
72
73#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
74pub struct ConcreteTypeId(pub String);
75
76impl From<&str> for ConcreteTypeId {
77    fn from(value: &str) -> Self {
78        ConcreteTypeId(value.into())
79    }
80}
81
82#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
83pub struct MetadataTypeId(pub usize);
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
86#[serde(untagged)]
87pub enum TypeId {
88    Concrete(ConcreteTypeId),
89    Metadata(MetadataTypeId),
90}
91
92impl Default for TypeId {
93    fn default() -> Self {
94        TypeId::Metadata(MetadataTypeId(usize::MAX))
95    }
96}
97
98#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99#[serde(rename_all = "camelCase")]
100pub struct ABIFunction {
101    pub name: String,
102    pub inputs: Vec<TypeConcreteParameter>,
103    pub output: ConcreteTypeId,
104    pub attributes: Option<Vec<Attribute>>,
105}
106
107impl ABIFunction {
108    pub fn is_payable(&self) -> bool {
109        self.attributes
110            .iter()
111            .flatten()
112            .any(|attr| attr.name == "payable")
113    }
114}
115
116#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117#[serde(rename_all = "camelCase")]
118pub struct TypeMetadataDeclaration {
119    #[serde(rename = "type")]
120    pub type_field: String,
121    pub metadata_type_id: MetadataTypeId,
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub components: Option<Vec<TypeApplication>>,
124    #[serde(skip_serializing_if = "Option::is_none")]
125    pub type_parameters: Option<Vec<MetadataTypeId>>,
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub alias_of: Option<MetadataTypeId>,
128}
129
130#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
131#[serde(rename_all = "camelCase")]
132pub struct TypeConcreteDeclaration {
133    #[serde(rename = "type")]
134    pub type_field: String,
135    pub concrete_type_id: ConcreteTypeId,
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub metadata_type_id: Option<MetadataTypeId>,
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub type_arguments: Option<Vec<ConcreteTypeId>>,
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub alias_of: Option<ConcreteTypeId>,
142}
143
144#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
145#[serde(rename_all = "camelCase")]
146pub struct TypeConcreteParameter {
147    pub name: String,
148    pub concrete_type_id: ConcreteTypeId,
149}
150
151#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
152#[serde(rename_all = "camelCase")]
153pub struct TypeApplication {
154    pub name: String,
155    pub type_id: TypeId,
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub error_message: Option<String>,
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub offset: Option<u16>,
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub type_arguments: Option<Vec<TypeApplication>>,
162}
163
164#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
165#[serde(rename_all = "camelCase")]
166pub struct LoggedType {
167    pub log_id: String,
168    pub concrete_type_id: ConcreteTypeId,
169}
170
171#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct MessageType {
174    pub message_id: String,
175    pub concrete_type_id: ConcreteTypeId,
176}
177
178#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
179#[serde(rename_all = "camelCase")]
180pub struct Configurable {
181    pub name: String,
182    pub concrete_type_id: ConcreteTypeId,
183    pub offset: u64,
184    #[serde(default)]
185    pub indirect: bool,
186}
187
188#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
189#[serde(rename_all = "camelCase")]
190pub struct Attribute {
191    pub name: String,
192    pub arguments: Vec<String>,
193}
194
195/// Represents a position in the source code where an error occurred.
196/// An error can be a direct call to panic, or a call site of a function
197/// that could panic.
198#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
199#[serde(rename_all = "camelCase")]
200pub struct ErrorPosition {
201    #[serde(default)]
202    /// The full-name of the enclosing function in which the error occurs.
203    pub function: String,
204    pub pkg: String,
205    pub file: String,
206    pub line: u64,
207    pub column: u64,
208}
209
210#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
211#[serde(rename_all = "camelCase")]
212pub struct ErrorDetails {
213    pub pos: ErrorPosition,
214    pub log_id: Option<String>,
215    pub msg: Option<String>,
216}
217
218/// Represents a call that could panic during execution.
219/// E.g., for the following code:
220///
221/// ```ignore
222/// fn some_function() {
223///    let _ = this_function_might_panic(42);
224///}
225/// ```
226///
227/// the ABI will contain a [PanickingCall] entry similar to:
228///
229/// ```ignore
230///  "pos": {
231///     "function": "some_package::some_module::some_function",
232///     "pkg": "some_package@0.1.0",
233///     "file": "src/some_module.sw",
234///     "line": 4,
235///     "column": 8
236///   },
237///   "function": "some_other_package::module::this_function_might_panic"
238/// ```
239#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
240#[serde(rename_all = "camelCase")]
241pub struct PanickingCall {
242    pub pos: ErrorPosition,
243    /// The full-name of the function whose call caused the panic.
244    pub function: String,
245}
246
247#[test]
248fn version_extraction_test() {
249    let v = Version("1.2".to_string());
250    assert_eq!(v.major(), Some("1"));
251    assert_eq!(v.minor(), Some("2"));
252
253    let v = Version("1".to_string());
254    assert_eq!(v.major(), Some("1"));
255    assert_eq!(v.minor(), None);
256
257    let v = Version("".to_string());
258    assert_eq!(v.major(), None);
259    assert_eq!(v.minor(), None);
260}
261
262// To see the JSON serialization printed, run the following command:
263//   cargo test serde_json_serialization_tryout -- --include-ignored --nocapture
264#[test]
265#[ignore = "not a test, just a convenient way to try the serialization out"]
266fn serde_json_serialization_tryout() {
267    let mut abi = ProgramABI::default();
268
269    abi.concrete_types.push(TypeConcreteDeclaration {
270        type_field: "()".into(),
271        concrete_type_id: ConcreteTypeId(
272            "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d".into(),
273        ),
274        metadata_type_id: None,
275        type_arguments: None,
276        alias_of: None,
277    });
278
279    abi.concrete_types.push(TypeConcreteDeclaration {
280        type_field: "enum MyError".into(),
281        concrete_type_id: ConcreteTypeId(
282            "44781f4b1eb667f225275b0a1c877dd4b9a8ab01f3cd01f8ed84f95c6cd2f363".into(),
283        ),
284        metadata_type_id: Some(MetadataTypeId(0)),
285        type_arguments: None,
286        alias_of: None,
287    });
288
289    abi.metadata_types.push(TypeMetadataDeclaration {
290        type_field: "enum MyError".into(),
291        metadata_type_id: MetadataTypeId(0),
292        components: Some(vec![TypeApplication {
293            offset: None,
294            name: "MyErrorVariant".into(),
295            type_id: TypeId::Concrete(ConcreteTypeId(
296                "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d".into(),
297            )),
298            error_message: Some("My error variant error message.".into()),
299            type_arguments: None,
300        }]),
301        type_parameters: None,
302        alias_of: None,
303    });
304
305    let mut error_codes = BTreeMap::new();
306
307    error_codes.insert(
308        0,
309        ErrorDetails {
310            pos: ErrorPosition {
311                function: "my_function".to_string(),
312                pkg: "my_lib".to_string(),
313                file: "lib.rs".to_string(),
314                line: 42,
315                column: 13,
316            },
317            log_id: None,
318            msg: Some("Error message.".to_string()),
319        },
320    );
321    error_codes.insert(
322        1,
323        ErrorDetails {
324            pos: ErrorPosition {
325                function: "another_function".to_string(),
326                pkg: "my_contract".to_string(),
327                file: "main.rs".to_string(),
328                line: 21,
329                column: 34,
330            },
331            log_id: Some("4933727799282657266".to_string()),
332            msg: None,
333        },
334    );
335
336    abi.error_codes = Some(error_codes);
337
338    let mut panicking_calls = BTreeMap::new();
339
340    panicking_calls.insert(
341        1,
342        PanickingCall {
343            pos: ErrorPosition {
344                function: "my_function".to_string(),
345                pkg: "my_lib".to_string(),
346                file: "lib.rs".to_string(),
347                line: 38,
348                column: 13,
349            },
350            function: "this_function_might_panic".to_string(),
351        },
352    );
353
354    abi.panicking_calls = Some(panicking_calls);
355
356    println!("{}", serde_json::to_string_pretty(&abi).unwrap());
357}