Skip to main content

aelf_client/dto/
mod.rs

1use aelf_proto::aelf::{ResourceTokenCharged, TransactionFeeCharged};
2use base64::Engine;
3use prost::Message;
4use serde::{Deserialize, Deserializer, Serialize};
5use serde_json::{Map, Value};
6use std::collections::{BTreeMap, HashMap};
7
8/// Chain status returned by the blockchain status endpoint.
9#[derive(Clone, Debug, Default, Serialize, Deserialize)]
10#[serde(rename_all = "PascalCase")]
11pub struct ChainStatusDto {
12    pub chain_id: String,
13    #[serde(default, deserialize_with = "block_height_map_as_default")]
14    pub branches: HashMap<String, i64>,
15    #[serde(default, deserialize_with = "block_height_map_as_default")]
16    pub not_linked_blocks: HashMap<String, i64>,
17    pub longest_chain_height: i64,
18    pub longest_chain_hash: String,
19    pub genesis_block_hash: String,
20    pub genesis_contract_address: String,
21    pub last_irreversible_block_hash: String,
22    pub last_irreversible_block_height: i64,
23    pub best_chain_hash: String,
24    pub best_chain_height: i64,
25}
26
27/// Block payload returned by block lookup endpoints.
28#[derive(Clone, Debug, Default, Serialize, Deserialize)]
29#[serde(rename_all = "PascalCase")]
30pub struct BlockDto {
31    pub block_hash: String,
32    pub header: BlockHeaderDto,
33    pub body: BlockBodyDto,
34}
35
36/// Block header subset returned by REST block queries.
37#[derive(Clone, Debug, Default, Serialize, Deserialize)]
38#[serde(rename_all = "PascalCase")]
39pub struct BlockHeaderDto {
40    pub height: i64,
41    #[serde(default)]
42    pub previous_block_hash: String,
43}
44
45/// Block body subset returned by REST block queries.
46#[derive(Clone, Debug, Default, Serialize, Deserialize)]
47#[serde(rename_all = "PascalCase")]
48pub struct BlockBodyDto {
49    #[serde(default)]
50    pub transactions: Vec<String>,
51}
52
53/// Transaction pool counters returned by pool status queries.
54#[derive(Clone, Debug, Default, Serialize, Deserialize)]
55#[serde(rename_all = "PascalCase")]
56pub struct TransactionPoolStatusOutput {
57    #[serde(default)]
58    pub queued: i64,
59    #[serde(default)]
60    pub validated: i64,
61}
62
63/// Input payload for node-side raw transaction generation.
64#[derive(Clone, Debug, Default, Serialize, Deserialize)]
65#[serde(rename_all = "PascalCase")]
66pub struct CreateRawTransactionInput {
67    pub from: String,
68    pub to: String,
69    pub ref_block_number: i64,
70    pub ref_block_hash: String,
71    pub method_name: String,
72    pub params: String,
73}
74
75/// Output payload returned by node-side raw transaction generation.
76#[derive(Clone, Debug, Default, Serialize, Deserialize)]
77#[serde(rename_all = "PascalCase")]
78pub struct CreateRawTransactionOutput {
79    pub raw_transaction: String,
80}
81
82/// Signed raw transaction execution request.
83#[derive(Clone, Debug, Default, Serialize, Deserialize)]
84#[serde(rename_all = "PascalCase")]
85pub struct ExecuteRawTransactionDto {
86    pub raw_transaction: String,
87    pub signature: String,
88}
89
90/// Input payload for broadcasting a raw transaction plus signature.
91#[derive(Clone, Debug, Default, Serialize, Deserialize)]
92#[serde(rename_all = "PascalCase")]
93pub struct SendRawTransactionInput {
94    pub transaction: String,
95    pub signature: String,
96    pub return_transaction: bool,
97}
98
99/// Output payload returned by `sendRawTransaction`.
100#[derive(Clone, Debug, Default, Serialize, Deserialize)]
101#[serde(rename_all = "PascalCase")]
102pub struct SendRawTransactionOutput {
103    #[serde(alias = "TransactionID")]
104    pub transaction_id: String,
105    #[serde(default)]
106    pub transaction: TransactionDto,
107}
108
109/// Input payload for broadcasting a fully signed raw transaction.
110#[derive(Clone, Debug, Default, Serialize, Deserialize)]
111#[serde(rename_all = "PascalCase")]
112pub struct SendTransactionInput {
113    pub raw_transaction: String,
114}
115
116/// Output payload returned by `sendTransaction`.
117#[derive(Clone, Debug, Default, Serialize, Deserialize)]
118#[serde(rename_all = "PascalCase")]
119pub struct SendTransactionOutput {
120    #[serde(alias = "TransactionID")]
121    pub transaction_id: String,
122}
123
124/// Input payload for batching multiple signed raw transactions.
125#[derive(Clone, Debug, Default, Serialize, Deserialize)]
126#[serde(rename_all = "PascalCase")]
127pub struct SendTransactionsInput {
128    pub raw_transactions: String,
129}
130
131/// REST-serialized transaction representation returned by some node endpoints.
132#[derive(Clone, Debug, Default, Serialize, Deserialize)]
133#[serde(rename_all = "PascalCase")]
134pub struct TransactionDto {
135    #[serde(default)]
136    pub from: String,
137    #[serde(default)]
138    pub to: String,
139    #[serde(default)]
140    pub ref_block_number: i64,
141    #[serde(default)]
142    pub ref_block_prefix: String,
143    #[serde(default)]
144    pub method_name: String,
145    #[serde(default)]
146    pub params: String,
147    #[serde(default)]
148    pub signature: String,
149}
150
151/// Log event emitted by a transaction execution result.
152#[derive(Clone, Debug, Default, Serialize, Deserialize)]
153#[serde(rename_all = "PascalCase")]
154pub struct LogEventDto {
155    pub address: String,
156    pub name: String,
157    #[serde(default, deserialize_with = "null_vec_as_default")]
158    pub indexed: Vec<String>,
159    #[serde(default, deserialize_with = "null_string_as_default")]
160    pub non_indexed: String,
161}
162
163/// Transaction execution result returned by transaction result queries.
164#[derive(Clone, Debug, Default, Serialize, Deserialize)]
165#[serde(rename_all = "PascalCase")]
166pub struct TransactionResultDto {
167    #[serde(alias = "TransactionID")]
168    pub transaction_id: String,
169    pub status: String,
170    #[serde(default, deserialize_with = "null_vec_as_default")]
171    pub logs: Vec<LogEventDto>,
172    #[serde(default, deserialize_with = "null_string_as_default")]
173    pub bloom: String,
174    #[serde(default, deserialize_with = "null_json_as_default")]
175    pub transaction: serde_json::Value,
176    #[serde(default, deserialize_with = "null_string_as_default")]
177    pub return_value: String,
178    #[serde(default, deserialize_with = "null_i64_as_default")]
179    pub block_number: i64,
180    #[serde(default, deserialize_with = "null_string_as_default")]
181    pub block_hash: String,
182    #[serde(default, deserialize_with = "null_string_as_default")]
183    pub error: String,
184}
185
186impl TransactionResultDto {
187    /// Extracts ELF and resource token fees from protobuf-encoded execution logs.
188    pub fn get_transaction_fees(&self) -> HashMap<String, i64> {
189        let engine = base64::engine::general_purpose::STANDARD;
190        let mut result = HashMap::new();
191
192        for log in &self.logs {
193            match log.name.as_str() {
194                "TransactionFeeCharged" => {
195                    let Ok(bytes) = engine.decode(&log.non_indexed) else {
196                        continue;
197                    };
198                    let Ok(event) = TransactionFeeCharged::decode(bytes.as_slice()) else {
199                        continue;
200                    };
201                    result.insert(event.symbol, event.amount);
202                }
203                "ResourceTokenCharged" => {
204                    let Ok(bytes) = engine.decode(&log.non_indexed) else {
205                        continue;
206                    };
207                    let Ok(event) = ResourceTokenCharged::decode(bytes.as_slice()) else {
208                        continue;
209                    };
210                    result.insert(event.symbol, event.amount);
211                }
212                _ => {}
213            }
214        }
215
216        result
217    }
218}
219
220fn null_string_as_default<'de, D>(deserializer: D) -> Result<String, D::Error>
221where
222    D: Deserializer<'de>,
223{
224    let value = Option::<String>::deserialize(deserializer)?;
225    Ok(value.unwrap_or_default())
226}
227
228fn null_vec_as_default<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
229where
230    D: Deserializer<'de>,
231    T: Deserialize<'de>,
232{
233    let value = Option::<Vec<T>>::deserialize(deserializer)?;
234    Ok(value.unwrap_or_default())
235}
236
237fn null_json_as_default<'de, D>(deserializer: D) -> Result<serde_json::Value, D::Error>
238where
239    D: Deserializer<'de>,
240{
241    let value = Option::<serde_json::Value>::deserialize(deserializer)?;
242    Ok(value.unwrap_or(serde_json::Value::Null))
243}
244
245fn null_i64_as_default<'de, D>(deserializer: D) -> Result<i64, D::Error>
246where
247    D: Deserializer<'de>,
248{
249    let value = Option::<i64>::deserialize(deserializer)?;
250    Ok(value.unwrap_or_default())
251}
252
253fn block_height_map_as_default<'de, D>(deserializer: D) -> Result<HashMap<String, i64>, D::Error>
254where
255    D: Deserializer<'de>,
256{
257    let value = Option::<Map<String, Value>>::deserialize(deserializer)?;
258    let mut result = HashMap::new();
259
260    for (key, value) in value.unwrap_or_default() {
261        match value {
262            Value::Number(number) => {
263                let height = number.as_i64().ok_or_else(|| {
264                    serde::de::Error::custom(format!(
265                        "block height value for '{key}' is not a signed integer"
266                    ))
267                })?;
268                result.insert(key, height);
269            }
270            Value::String(text) => {
271                if let Ok(height) = text.parse::<i64>() {
272                    result.insert(key, height);
273                    continue;
274                }
275
276                let inverted_height = key.parse::<i64>().map_err(|_| {
277                    serde::de::Error::custom(format!(
278                        "invalid block height map entry '{key}': '{text}'"
279                    ))
280                })?;
281                result.insert(text, inverted_height);
282            }
283            other => {
284                return Err(serde::de::Error::custom(format!(
285                    "invalid block height map value for '{key}': {other}"
286                )));
287            }
288        }
289    }
290
291    Ok(result)
292}
293
294/// Merkle path node returned by transaction proof queries.
295#[derive(Clone, Debug, Default, Serialize, Deserialize)]
296#[serde(rename_all = "PascalCase")]
297pub struct MerklePathNodeDto {
298    pub hash: String,
299    pub is_left_child_node: bool,
300}
301
302/// Merkle path returned by transaction proof queries.
303#[derive(Clone, Debug, Default, Serialize, Deserialize)]
304#[serde(rename_all = "PascalCase")]
305pub struct MerklePathDto {
306    #[serde(default)]
307    pub merkle_path_nodes: Vec<MerklePathNodeDto>,
308}
309
310/// Peer description returned by the network peers endpoint.
311#[derive(Clone, Debug, Default, Serialize, Deserialize)]
312#[serde(rename_all = "PascalCase")]
313pub struct PeerDto {
314    #[serde(default)]
315    pub ip_address: String,
316    #[serde(flatten)]
317    pub extra: BTreeMap<String, serde_json::Value>,
318}
319
320/// Network metadata returned by the network info endpoint.
321#[derive(Clone, Debug, Default, Serialize, Deserialize)]
322#[serde(rename_all = "PascalCase")]
323pub struct NetworkInfoOutput {
324    #[serde(default)]
325    pub version: String,
326    #[serde(flatten)]
327    pub extra: BTreeMap<String, serde_json::Value>,
328}
329
330/// Task queue snapshot returned by the task queue endpoint.
331#[derive(Clone, Debug, Default, Serialize, Deserialize)]
332#[serde(rename_all = "PascalCase")]
333pub struct TaskQueueInfoDto {
334    #[serde(flatten)]
335    pub extra: BTreeMap<String, serde_json::Value>,
336}
337
338/// Input payload for the transaction fee estimation endpoint.
339#[derive(Clone, Debug, Default, Serialize, Deserialize)]
340#[serde(rename_all = "PascalCase")]
341pub struct CalculateTransactionFeeInput {
342    pub raw_transaction: String,
343}
344
345/// Output payload returned by the transaction fee estimation endpoint.
346#[derive(Clone, Debug, Default, Serialize, Deserialize)]
347#[serde(rename_all = "PascalCase")]
348pub struct CalculateTransactionFeeOutput {
349    pub success: bool,
350    #[serde(default)]
351    pub transaction_fee: HashMap<String, f64>,
352}
353
354/// Standard WebApp error envelope returned by AElf node REST APIs.
355#[derive(Clone, Debug, Default, Serialize, Deserialize)]
356pub struct WebAppErrorResponse {
357    pub error: WebAppError,
358}
359
360/// Error payload nested inside the WebApp error envelope.
361#[derive(Clone, Debug, Default, Serialize, Deserialize)]
362pub struct WebAppError {
363    pub code: String,
364    pub message: String,
365    #[serde(default)]
366    pub details: Option<String>,
367}
368
369#[cfg(test)]
370mod tests {
371    use super::{ChainStatusDto, LogEventDto, TransactionResultDto};
372    use aelf_proto::aelf::{Address, ResourceTokenCharged, TransactionFeeCharged};
373    use base64::Engine;
374    use prost::Message;
375    use serde_json::json;
376
377    #[test]
378    fn parses_transaction_fee_logs() {
379        let engine = base64::engine::general_purpose::STANDARD;
380        let tx_fee = TransactionFeeCharged {
381            symbol: "ELF".to_owned(),
382            amount: 12_345,
383        };
384        let resource_fee = ResourceTokenCharged {
385            symbol: "CPU".to_owned(),
386            amount: 999,
387            contract_address: Some(Address {
388                value: vec![1_u8; 32],
389            }),
390        };
391        let result = TransactionResultDto {
392            transaction_id: "0x01".to_owned(),
393            status: "MINED".to_owned(),
394            logs: vec![
395                LogEventDto {
396                    address: "addr".to_owned(),
397                    name: "TransactionFeeCharged".to_owned(),
398                    indexed: Vec::new(),
399                    non_indexed: engine.encode(tx_fee.encode_to_vec()),
400                },
401                LogEventDto {
402                    address: "addr".to_owned(),
403                    name: "ResourceTokenCharged".to_owned(),
404                    indexed: Vec::new(),
405                    non_indexed: engine.encode(resource_fee.encode_to_vec()),
406                },
407            ],
408            bloom: String::new(),
409            transaction: serde_json::Value::Null,
410            return_value: String::new(),
411            block_number: 1,
412            block_hash: "0x02".to_owned(),
413            error: String::new(),
414        };
415
416        let fees = result.get_transaction_fees();
417        assert_eq!(fees.get("ELF"), Some(&12_345));
418        assert_eq!(fees.get("CPU"), Some(&999));
419    }
420
421    #[test]
422    fn parses_chain_status_hash_to_height_maps() {
423        let status: ChainStatusDto = serde_json::from_value(json!({
424            "ChainId": "AELF",
425            "Branches": {
426                "abc": 42
427            },
428            "NotLinkedBlocks": null,
429            "LongestChainHeight": 42,
430            "LongestChainHash": "abc",
431            "GenesisBlockHash": "genesis",
432            "GenesisContractAddress": "contract",
433            "LastIrreversibleBlockHash": "lib",
434            "LastIrreversibleBlockHeight": 40,
435            "BestChainHash": "abc",
436            "BestChainHeight": 42
437        }))
438        .expect("chain status");
439
440        assert_eq!(status.branches.get("abc"), Some(&42));
441        assert!(status.not_linked_blocks.is_empty());
442    }
443
444    #[test]
445    fn parses_chain_status_height_to_hash_maps() {
446        let status: ChainStatusDto = serde_json::from_value(json!({
447            "ChainId": "AELF",
448            "Branches": {
449                "42": "abc"
450            },
451            "NotLinkedBlocks": {
452                "7": "def"
453            },
454            "LongestChainHeight": 42,
455            "LongestChainHash": "abc",
456            "GenesisBlockHash": "genesis",
457            "GenesisContractAddress": "contract",
458            "LastIrreversibleBlockHash": "lib",
459            "LastIrreversibleBlockHeight": 40,
460            "BestChainHash": "abc",
461            "BestChainHeight": 42
462        }))
463        .expect("chain status");
464
465        assert_eq!(status.branches.get("abc"), Some(&42));
466        assert_eq!(status.not_linked_blocks.get("def"), Some(&7));
467    }
468}