aptos_network_sdk/
trade.rs

1use crate::{
2    Aptos,
3    types::{ContractCall, EntryFunctionPayload},
4    wallet::Wallet,
5};
6use aptos_network_tool::{address::address_to_bytes, signature::serialize_transaction_and_sign};
7use futures::future::join_all;
8use serde::{Deserialize, Serialize};
9use serde_json::{Value, json};
10use std::{
11    collections::{HashMap, HashSet},
12    sync::Arc,
13    time::{SystemTime, UNIX_EPOCH},
14};
15use tokio::sync::Semaphore;
16
17pub struct Trade;
18
19impl Trade {
20    /// build transfer info
21    pub async fn create_transfer_tx(
22        client: Arc<Aptos>,
23        sender: Arc<Wallet>,
24        recipient: &str,
25        amount: u64,
26        sequence_number: Option<u64>,
27        expiration_secs: u64,
28        max_gas_amount: u64,
29        gas_unit_price: u64,
30    ) -> Result<Value, String> {
31        let sequence_number = match sequence_number {
32            Some(seq) => seq,
33            None => {
34                let account_info = client
35                    .get_account_info(&sender.address().unwrap())
36                    .await
37                    .unwrap();
38                account_info.sequence_number.parse().unwrap()
39            }
40        };
41        let current_timestamp = SystemTime::now()
42            .duration_since(UNIX_EPOCH)
43            .unwrap()
44            .as_secs();
45        let expiration_timestamp = current_timestamp + expiration_secs;
46        // build transaction payload
47        let payload = json!({
48            "type": "entry_function_payload",
49            "function": "0x1::coin::transfer",
50            "type_arguments": ["0x1::aptos_coin::AptosCoin"],
51            "arguments": [recipient, amount.to_string()]
52        });
53        // build raw transaction
54        let raw_txn = json!({
55            "sender": sender.address(),
56            "sequence_number": sequence_number.to_string(),
57            "max_gas_amount": max_gas_amount.to_string(),
58            "gas_unit_price": gas_unit_price.to_string(),
59            "expiration_timestamp_secs": expiration_timestamp.to_string(),
60            "payload": payload
61        });
62        Ok(raw_txn)
63    }
64
65    /// build token transfer
66    pub async fn create_token_transfer_tx(
67        client: Arc<Aptos>,
68        sender: Wallet,
69        recipient: &str,
70        token_type: &str,
71        amount: u64,
72        sequence_number: Option<u64>,
73        expiration_secs: u64,
74        max_gas_amount: u64,
75        gas_unit_price: u64,
76    ) -> Result<Value, String> {
77        let chain_id = client.get_chain_info().await.unwrap().chain_id;
78        let sequence_number = match sequence_number {
79            Some(seq) => seq,
80            None => {
81                client
82                    .get_account_sequence_number(&sender.address()?)
83                    .await?
84            }
85        };
86        let current_timestamp = SystemTime::now()
87            .duration_since(UNIX_EPOCH)
88            .unwrap()
89            .as_secs();
90        let expiration_timestamp = current_timestamp + expiration_secs;
91        // build transaction payload
92        let payload = json!({
93            "type": "entry_function_payload",
94            "function": "0x1::coin::transfer",
95            "type_arguments": [token_type],
96            "arguments": [recipient, amount.to_string()]
97        });
98        // build raw transaction
99        let raw_txn = json!({
100            "sender": sender.address().unwrap(),
101            "sequence_number": sequence_number.to_string(),
102            "max_gas_amount": max_gas_amount.to_string(),
103            "gas_unit_price": gas_unit_price.to_string(),
104            "expiration_timestamp_secs": expiration_timestamp.to_string(),
105            "payload": payload,
106            "chain_id": chain_id
107        });
108        Ok(raw_txn)
109    }
110
111    /// create sign and submit transfer tx
112    pub async fn create_sign_submit_transfer_tx(
113        client: Arc<Aptos>,
114        wallet: Arc<Wallet>,
115        recipient: &str,
116        amount: u64,
117        sequence_number: Option<u64>,
118        expiration_secs: u64,
119        max_gas_amount: u64,
120        gas_unit_price: u64,
121    ) -> Result<String, String> {
122        // build raw transaction
123        let raw_txn = Trade::create_transfer_tx(
124            Arc::clone(&client),
125            Arc::clone(&wallet),
126            recipient,
127            amount,
128            sequence_number,
129            expiration_secs,
130            max_gas_amount,
131            gas_unit_price,
132        )
133        .await
134        .unwrap();
135        // serialize transaction and sign
136        let message_to_sign = serialize_transaction_and_sign(&raw_txn)?;
137        // wallet sign
138        match wallet.sign(&message_to_sign) {
139            Ok(signature_bytes) => {
140                // create signed transaction tx
141                match Trade::create_signed_transaction_tx(
142                    Arc::clone(&wallet),
143                    raw_txn,
144                    signature_bytes,
145                ) {
146                    Ok(signed_txn) => {
147                        // submit transaction
148                        match client.submit_transaction(&signed_txn).await {
149                            Ok(result) => {
150                                return Ok(result.hash);
151                            }
152                            Err(e) => return Err(format!("submit transaction error: {:?}", e)),
153                        }
154                    }
155                    Err(e) => return Err(format!("build signed transaction error: {:?}", e)),
156                }
157            }
158            Err(e) => {
159                return Err(format!("wallet sign error:{:?}", e).to_string());
160            }
161        }
162    }
163
164    /// build call contract tx
165    pub async fn create_call_contract_tx(
166        client: Arc<Aptos>,
167        sender: Arc<Wallet>,
168        sequence_number: Option<u64>,
169        expiration_secs: u64,
170        max_gas_amount: u64,
171        gas_unit_price: u64,
172        payload: EntryFunctionPayload,
173    ) -> Result<Value, String> {
174        let sequence_number = match sequence_number {
175            Some(seq) => seq,
176            None => client
177                .get_account_sequence_number(&sender.address().unwrap())
178                .await
179                .unwrap(),
180        };
181        let chain_id = client.get_chain_info().await.unwrap().chain_id;
182        // current timestamp
183        let current_timestamp = std::time::SystemTime::now()
184            .duration_since(std::time::UNIX_EPOCH)
185            .unwrap()
186            .as_secs();
187        // expiration time
188        let expiration_timestamp = current_timestamp + expiration_secs;
189        // build raw transaction
190        let raw_txn = json!({
191            "sender": sender.address()?,
192            "sequence_number": sequence_number.to_string(),
193            "max_gas_amount": max_gas_amount.to_string(),
194            "gas_unit_price": gas_unit_price.to_string(),
195            "expiration_timestamp_secs": expiration_timestamp.to_string(),
196            "payload": payload,
197            "chain_id": chain_id
198        });
199        Ok(raw_txn)
200    }
201
202    /// create customize call contract tx
203    pub async fn create_customize_call_contract_tx(
204        client: Arc<Aptos>,
205        module_address: &str,
206        module_name: &str,
207        function_name: &str,
208        type_arguments: Vec<String>,
209        arguments: Vec<Value>,
210        sender: Arc<Wallet>,
211        sequence_number: Option<u64>,
212        expiration_secs: u64,
213        max_gas_amount: u64,
214        gas_unit_price: u64,
215    ) -> Result<Value, String> {
216        let function_str = format!("{}::{}::{}", module_address, module_name, function_name);
217        let function_vec = function_str.as_bytes().to_vec();
218        let mut type_args: Vec<Vec<u8>> = Vec::new();
219        type_arguments
220            .iter()
221            .for_each(|s| type_args.push(s.as_bytes().to_vec()));
222        let mut args: Vec<Vec<u8>> = Vec::new();
223        arguments
224            .iter()
225            .for_each(|s| args.push(s.as_str().unwrap().to_string().as_bytes().to_vec()));
226        let payload = EntryFunctionPayload {
227            module_address: address_to_bytes(module_address).unwrap().to_vec(),
228            module_name: address_to_bytes(module_name).unwrap().to_vec(),
229            function_name: function_vec,
230            type_arguments: type_args,
231            arguments: args,
232        };
233        Trade::create_call_contract_tx(
234            client,
235            sender,
236            sequence_number,
237            expiration_secs,
238            max_gas_amount,
239            gas_unit_price,
240            payload,
241        )
242        .await
243    }
244
245    ///  build signed transaction
246    pub fn create_signed_transaction_tx(
247        wallet: Arc<Wallet>,
248        raw_txn: Value,
249        signature: Vec<u8>,
250    ) -> Result<Value, String> {
251        let public_key_hex = wallet
252            .public_key_hex()
253            .map_err(|e| format!("get public key hex: {}", e))?;
254        Ok(json!({
255            "transaction": raw_txn,
256            "signature": {
257                "type": "ed25519_signature",
258                "public_key": public_key_hex,
259                "signature": hex::encode(signature)
260            }
261        }))
262    }
263
264    /// Retrieves transaction history for a specified address with pagination support
265    ///
266    /// # Params
267    /// client - Aptos Client
268    /// address - address
269    /// query - query:
270    ///     - start - page
271    ///     - limit - page size
272    ///
273    /// # Returns
274    /// Ok(Vec<Transaction>) - Transaction vec
275    /// Err(String) - Error
276    ///
277    /// # Examples
278    /// ```rust
279    /// use std::sync::Arc;
280    ///
281    /// let client = Arc::new(Aptos::new(AptosType::Mainnet));
282    /// let query = TransactionQuery {
283    ///     start: Some(0),
284    ///     limit: Some(10)
285    /// };
286    ///
287    /// match Trade::get_address_transactions(client, "0x1234...", query).await {
288    ///     Ok(transactions) => println!("Search {} transactions", transactions.len()),
289    ///     Err(e) => println!("Error: {}", e),
290    /// }
291    /// ```
292    pub async fn get_address_transactions(
293        client: Arc<Aptos>,
294        address: &str,
295        query: TransactionQuery,
296    ) -> Result<Vec<TransactionInfo>, String> {
297        client
298            .get_account_transaction_vec(address, query.limit, query.start)
299            .await
300    }
301
302    /// Filters transactions from address_a to only include those involving address_b
303    ///
304    /// # Params
305    /// client - aptos client
306    /// address_a - The primary account address to fetch transactions from
307    /// address_b - Address B used as filtering condition
308    /// limit - data limit
309    /// start - starting sequence number for pagination (Optional)
310    ///
311    /// # Returns
312    /// Ok(Vec<Transaction>) - Filtered vector of transactions where both addresses are involved
313    /// Err(String) - Error message if the request fails
314    ///
315    /// # Examples
316    /// ```
317    /// use std::sync::Arc;
318    ///
319    /// let client = Arc::new(Aptos::new(AptosType::Mainnet));
320    ///
321    /// // Find all transactions where 0x1234... and 0x5678... interacted
322    /// match Trade::get_transactions_involving_both_addresses(
323    ///     client,
324    ///     "0x1234...",
325    ///     "0x5678...",
326    ///     Some(50),  // Limit to 50 transactions
327    ///     None       // Start from most recent
328    /// ).await {
329    ///     Ok(shared_transactions) => {
330    ///         println!("Search {} shared transactions", shared_transactions.len());
331    ///     },
332    ///     Err(e) => println!("Error: {}", e),
333    /// }
334    /// ```
335    ///
336    pub async fn get_transactions_involving_both_addresses(
337        client: Arc<Aptos>,
338        address_a: &str,
339        address_b: &str,
340        limit: Option<u64>,
341        start: Option<u64>,
342    ) -> Result<Vec<TransactionInfo>, String> {
343        let query = TransactionQuery { start, limit };
344        let transactions = Self::get_address_transactions(client, address_a, query).await?;
345        let filtered_transactions: Vec<TransactionInfo> = transactions
346            .into_iter()
347            .filter(|txn| Self::transaction_involves_address(txn, address_b))
348            .collect();
349        Ok(filtered_transactions)
350    }
351
352    /// Retrieves transactions where address_b is the sender and address_a is the recipient
353    ///
354    /// This function specifically finds transfer transactions where `address_b` (payer) sent funds
355    /// to `address_a` (recipient). It searches through `address_b`'s transaction history and
356    /// filters for coin transfer operations targeting `address_a`.
357    ///
358    /// # Params
359    /// client - aptos client
360    /// address_a - The primary account address to fetch transactions from
361    /// address_b - Address B used as filtering condition
362    /// limit - data limit
363    /// start - starting sequence number for pagination (Optional)
364    ///
365    /// # Returns
366    /// Ok(Vec<Transaction>) - Transaction vec
367    /// Err(String) - Error message
368    ///
369    /// # Examples
370    /// ```
371    /// use std::sync::Arc;
372    ///
373    /// let client = Arc::new(Aptos::new(AptosType::Mainnet));
374    ///
375    /// // Find all payments from Alice (0x5678...) to Bob (0x1234...)
376    /// match Trade::get_transactions_by_recipient_sender(
377    ///     client,
378    ///     "0x1234...",  // address A - recipient
379    ///     "0x5678...",  // address B - sender
380    ///     Some(100),    // Limit to 100 transactions
381    ///     None          // Start from most recent
382    /// ).await {
383    ///     Ok(payments) => {
384    ///         println!("Found {} payments from Alice to Bob", payments.len());
385    ///         for payment in payments {
386    ///             if let Some(transfer_info) = Trade::get_transfer_info(&payment) {
387    ///                 println!("Amount: {} {}", transfer_info.amount, transfer_info.token_type);
388    ///             }
389    ///         }
390    ///     },
391    ///     Err(e) => println!("Error: {}", e),
392    /// }
393    /// ```
394    ///
395    pub async fn get_transactions_by_recipient_sender(
396        client: Arc<Aptos>,
397        address_a: &str, // Receiver
398        address_b: &str, // Payer
399        limit: Option<u64>,
400        start: Option<u64>,
401    ) -> Result<Vec<TransactionInfo>, String> {
402        let query = TransactionQuery { start, limit };
403        let transactions =
404            Self::get_address_transactions(Arc::clone(&client), address_b, query).await?;
405        let filtered_transactions: Vec<TransactionInfo> = transactions
406            .into_iter()
407            .filter(|txn| Self::is_transfer_from_to(txn, address_b, address_a))
408            .collect();
409        Ok(filtered_transactions)
410    }
411
412    /// Check if the transaction involves the specified address
413    fn transaction_involves_address(transaction: &TransactionInfo, address: &str) -> bool {
414        match &transaction.transaction_type {
415            TransactionType::UserTransaction(user_txn) => {
416                // Check sender
417                if user_txn.sender == address {
418                    return true;
419                }
420                // Check the address parameter in the payload
421                Self::payload_contains_address(&user_txn.payload, address)
422            }
423            TransactionType::PendingTransaction(pending_txn) => {
424                pending_txn.sender == address
425                    || Self::payload_contains_address(&pending_txn.payload, address)
426            }
427            _ => false,
428        }
429    }
430
431    /// Check if the transaction is a transfer from from_address to to_address
432    fn is_transfer_from_to(
433        transaction: &TransactionInfo,
434        from_address: &str,
435        to_address: &str,
436    ) -> bool {
437        match &transaction.transaction_type {
438            TransactionType::UserTransaction(user_txn) => {
439                if user_txn.sender != from_address {
440                    return false;
441                }
442                Self::is_transfer_to_address(&user_txn.payload, to_address)
443            }
444            TransactionType::PendingTransaction(pending_txn) => {
445                if pending_txn.sender != from_address {
446                    return false;
447                }
448                Self::is_transfer_to_address(&pending_txn.payload, to_address)
449            }
450            _ => false,
451        }
452    }
453
454    /// Check if the payload contains the specified address
455    fn payload_contains_address(payload: &Payload, address: &str) -> bool {
456        for arg in &payload.arguments {
457            if let Some(arg_str) = arg.as_str() {
458                if arg_str == address {
459                    return true;
460                }
461            }
462        }
463        false
464    }
465
466    /// Check if the payload is a transfer to the specified address
467    fn is_transfer_to_address(payload: &Payload, recipient_address: &str) -> bool {
468        if payload.function.ends_with("::coin::transfer") {
469            if let Some(first_arg) = payload.arguments.first() {
470                if let Some(recipient) = first_arg.as_str() {
471                    return recipient == recipient_address;
472                }
473            }
474        }
475        false
476    }
477
478    /// Get user transaction details
479    pub fn get_user_transaction(transaction: &TransactionInfo) -> Option<&UserTransaction> {
480        match &transaction.transaction_type {
481            TransactionType::UserTransaction(user_txn) => Some(user_txn),
482            _ => None,
483        }
484    }
485
486    /// Get transfer information in the transaction
487    pub fn get_transfer_info(transaction: &TransactionInfo) -> Option<TransferInfo> {
488        let user_txn = Self::get_user_transaction(transaction)?;
489        if user_txn.payload.function.ends_with("::coin::transfer") {
490            if user_txn.payload.arguments.len() >= 2 {
491                let recipient = user_txn.payload.arguments[0].as_str()?.to_string();
492                let amount = user_txn.payload.arguments[1].as_str()?.parse().ok()?;
493                // Extract token type
494                let token_type = if !user_txn.payload.type_arguments.is_empty() {
495                    user_txn.payload.type_arguments[0].clone()
496                } else {
497                    "0x1::aptos_coin::AptosCoin".to_string()
498                };
499                Some(TransferInfo {
500                    from: user_txn.sender.clone(),
501                    to: recipient,
502                    amount,
503                    token_type,
504                })
505            } else {
506                None
507            }
508        } else {
509            None
510        }
511    }
512
513    /// Get events of a specific type in transaction events
514    pub fn get_events_by_type<'a>(
515        transaction: &'a TransactionInfo,
516        event_type: &str,
517    ) -> Vec<&'a Event> {
518        transaction
519            .events
520            .iter()
521            .filter(|event| event.r#type.contains(event_type))
522            .collect()
523    }
524
525    /// Analyze resource changes in transactions
526    pub fn analyze_resource_changes(transaction: &TransactionInfo) -> ResourceChanges {
527        let mut changes = ResourceChanges::default();
528        for change in &transaction.changes {
529            match change.change_type.as_str() {
530                "write_resource" => {
531                    if let Some(_data) = &change.data {
532                        changes.resources_modified += 1;
533                    }
534                }
535                "write_table_item" => {
536                    changes.table_items_modified += 1;
537                }
538                "delete_resource" => {
539                    changes.resources_deleted += 1;
540                }
541                "delete_table_item" => {
542                    changes.table_items_deleted += 1;
543                }
544                _ => {}
545            }
546        }
547        changes
548    }
549}
550
551/// batch transaction processor
552pub struct BatchTradeHandle;
553
554impl BatchTradeHandle {
555    /// Processing batch transactions with concurrency control
556    pub async fn process_batch(
557        client: Arc<Aptos>,
558        wallet: Arc<Wallet>,
559        calls: Vec<ContractCall>,
560        concurrency: usize,
561    ) -> Result<Vec<Value>, String> {
562        let semaphore = Arc::new(Semaphore::new(concurrency));
563        let mut tasks = Vec::new();
564        for call in calls {
565            let client_clone = Arc::clone(&client);
566            let wallet_clone = Arc::clone(&wallet);
567            let semaphore_clone = Arc::clone(&semaphore);
568
569            let task = async move {
570                let _permit = semaphore_clone.acquire().await.map_err(|e| e.to_string())?;
571                match crate::contract::Contract::write(client_clone, wallet_clone, call).await {
572                    Ok(result) => Ok(json!(result)),
573                    Err(e) => Err(e),
574                }
575            };
576            tasks.push(task);
577        }
578        let results = join_all(tasks).await;
579        let mut final_results = Vec::new();
580        for result in results {
581            match result {
582                Ok(value) => final_results.push(value),
583                Err(e) => final_results.push(json!({
584                    "success": false,
585                    "error": e
586                })),
587            }
588        }
589        Ok(final_results)
590    }
591
592    /// Read resources in batches
593    pub async fn batch_get_resources(
594        client: Arc<Aptos>,
595        addresses: Vec<String>,
596        resource_types: Vec<&str>,
597    ) -> Result<HashMap<String, HashMap<String, Option<Value>>>, String> {
598        let mut all_results = HashMap::new();
599        for address in addresses {
600            match crate::contract::Contract::batch_get_resources(
601                Arc::clone(&client),
602                &address,
603                resource_types.clone(),
604            )
605            .await
606            {
607                Ok(resources) => {
608                    all_results.insert(address, resources);
609                }
610                Err(e) => {
611                    eprintln!("Failed to get resources for address: {}", e);
612                }
613            }
614        }
615        Ok(all_results)
616    }
617}
618
619/// Represents a transaction on the Aptos blockchain
620/// Contains all relevant information about a transaction including metadata, payload, and execution results
621#[derive(Debug, Clone, Serialize, Deserialize)]
622pub struct TransactionInfo {
623    pub version: String,
624    pub hash: String,
625    #[serde(default)]
626    pub state_change_hash: String,
627    #[serde(default)]
628    pub event_root_hash: String,
629    pub state_checkpoint_hash: Option<String>,
630    #[serde(default)]
631    pub gas_used: String,
632    pub success: bool,
633    #[serde(default)]
634    pub vm_status: String,
635    #[serde(default)]
636    pub accumulator_root_hash: String,
637    #[serde(default)]
638    pub changes: Vec<WriteSetChange>,
639    #[serde(default)]
640    pub events: Vec<Event>,
641    #[serde(default)]
642    pub timestamp: Option<String>,
643    #[serde(default)]
644    pub max_gas_amount: Option<String>,
645    #[serde(flatten)]
646    pub transaction_type: TransactionType,
647}
648
649#[derive(Debug, Clone, Serialize, Deserialize)]
650#[serde(tag = "type")]
651pub enum TransactionType {
652    #[serde(rename = "pending_transaction")]
653    PendingTransaction(PendingTransaction),
654    #[serde(rename = "user_transaction")]
655    UserTransaction(UserTransaction),
656    #[serde(rename = "genesis_transaction")]
657    GenesisTransaction(GenesisTransaction),
658    #[serde(rename = "block_metadata_transaction")]
659    BlockMetadataTransaction(BlockMetadataTransaction),
660    #[serde(rename = "state_checkpoint_transaction")]
661    StateCheckpointTransaction(StateCheckpointTransaction),
662}
663
664#[derive(Debug, Clone, Serialize, Deserialize)]
665pub struct PendingTransaction {
666    pub hash: String,
667    pub sender: String,
668    pub sequence_number: String,
669    pub max_gas_amount: String,
670    pub gas_unit_price: String,
671    pub expiration_timestamp_secs: String,
672    pub payload: Payload,
673    pub signature: Option<Signature>,
674}
675
676#[derive(Debug, Clone, Serialize, Deserialize)]
677pub struct UserTransaction {
678    pub sender: String,
679    pub sequence_number: String,
680    #[serde(default)]
681    pub max_gas_amount: Option<String>,
682    #[serde(default)]
683    pub gas_unit_price: Option<String>,
684    #[serde(default)]
685    pub expiration_timestamp_secs: Option<String>,
686    pub payload: Payload,
687    pub signature: Signature,
688}
689
690#[derive(Debug, Clone, Serialize, Deserialize)]
691pub struct GenesisTransaction {
692    pub payload: Payload,
693    pub events: Vec<Event>,
694}
695
696#[derive(Debug, Clone, Serialize, Deserialize)]
697pub struct BlockMetadataTransaction {
698    pub id: String,
699    pub epoch: String,
700    pub round: String,
701    pub proposer: String,
702    pub failed_proposer_indices: Vec<u64>,
703    pub previous_block_votes_bitvec: Vec<u8>,
704    pub timestamp: String,
705    pub events: Vec<Event>,
706}
707
708#[derive(Debug, Clone, Serialize, Deserialize)]
709pub struct StateCheckpointTransaction {
710    pub timestamp: String,
711    pub version: String,
712    pub hash: String,
713    pub state_change_hash: String,
714    pub event_root_hash: String,
715    pub state_checkpoint_hash: Option<String>,
716    pub gas_used: String,
717    pub success: bool,
718    pub vm_status: String,
719    pub accumulator_root_hash: String,
720    pub changes: Vec<WriteSetChange>,
721    pub events: Vec<Event>,
722}
723
724#[derive(Debug, Clone, Serialize, Deserialize)]
725pub struct Payload {
726    #[serde(rename = "type")]
727    pub payload_type: String,
728    pub function: String,
729    pub type_arguments: Vec<String>,
730    pub arguments: Vec<Value>,
731    #[serde(skip_serializing_if = "Option::is_none")]
732    pub code: Option<Code>,
733}
734
735#[derive(Debug, Clone, Serialize, Deserialize)]
736pub struct Code {
737    pub bytecode: String,
738}
739
740#[derive(Debug, Clone, Serialize, Deserialize)]
741#[serde(tag = "type")]
742pub enum Signature {
743    #[serde(rename = "ed25519_signature")]
744    Ed25519 {
745        public_key: String,
746        signature: String,
747    },
748    #[serde(rename = "multi_ed25519_signature")]
749    MultiEd25519 {
750        public_keys: Vec<String>,
751        signatures: Vec<String>,
752        threshold: u8,
753    },
754    #[serde(rename = "single_key_signature")]
755    SingleKey {
756        public_key: String,
757        signature: String,
758    },
759    #[serde(rename = "multi_key_signature")]
760    MultiKey {
761        public_keys: Vec<String>,
762        signatures: Vec<String>,
763        threshold: u8,
764    },
765    #[serde(rename = "fee_payer_signature")]
766    FeePayer {
767        sender: Box<Signature>,
768        #[serde(default)]
769        fee_payer: Option<Box<Signature>>,
770    },
771}
772
773#[derive(Debug, Clone, Serialize, Deserialize)]
774pub struct Event {
775    pub guid: Guid,
776    pub sequence_number: String,
777    pub r#type: String,
778    pub data: Value,
779}
780
781#[derive(Debug, Clone, Serialize, Deserialize)]
782pub struct Guid {
783    pub creation_number: String,
784    pub account_address: String,
785}
786
787#[derive(Debug, Clone, Serialize, Deserialize)]
788
789pub struct WriteSetChange {
790    #[serde(rename = "type")]
791    pub change_type: String,
792    pub address: Option<String>,
793    pub state_key_hash: String,
794    pub data: Option<Value>,
795    pub handle: Option<String>,
796    pub key: Option<String>,
797    pub value: Option<String>,
798}
799
800#[derive(Debug, Clone, Serialize, Deserialize)]
801pub struct SubmitTransactionRequest {
802    pub sender: String,
803    pub sequence_number: String,
804    pub max_gas_amount: String,
805    pub gas_unit_price: String,
806    pub expiration_timestamp_secs: String,
807    pub payload: Payload,
808    pub signature: Signature,
809}
810
811#[derive(Debug, Clone, Serialize, Deserialize)]
812pub struct TransactionQuery {
813    pub start: Option<u64>,
814    pub limit: Option<u64>,
815}
816
817#[derive(Debug, Clone, Serialize, Deserialize)]
818pub struct TransferInfo {
819    pub from: String,
820    pub to: String,
821    pub amount: u64,
822    pub token_type: String,
823}
824
825#[derive(Debug, Clone, Serialize, Deserialize, Default)]
826pub struct ResourceChanges {
827    pub resources_modified: usize,
828    pub resources_deleted: usize,
829    pub table_items_modified: usize,
830    pub table_items_deleted: usize,
831}
832
833impl TransactionInfo {
834    /// Check if the transaction was successful
835    pub fn is_successful(&self) -> bool {
836        self.success
837    }
838
839    /// Get transaction timestamp
840    pub fn get_timestamp(&self) -> Option<u64> {
841        self.timestamp.clone().unwrap_or(0.to_string()).parse().ok()
842    }
843
844    /// Get the amount of gas used
845    pub fn get_gas_used(&self) -> Option<u64> {
846        self.gas_used.parse().ok()
847    }
848
849    /// Check whether it is a user transaction
850    pub fn is_user_transaction(&self) -> bool {
851        matches!(self.transaction_type, TransactionType::UserTransaction(_))
852    }
853
854    /// Get the sender address in this transaction.
855    pub fn get_sender(&self) -> Option<&str> {
856        match &self.transaction_type {
857            TransactionType::UserTransaction(user_txn) => Some(&user_txn.sender),
858            TransactionType::PendingTransaction(pending_txn) => Some(&pending_txn.sender),
859            _ => None,
860        }
861    }
862
863    fn extract_received_from_event(event: &Event) -> Vec<(String, u64)> {
864        let mut result = Vec::new();
865        if let serde_json::Value::Object(data) = &event.data {
866            if event.r#type.contains("Swap") {
867                if let (Some(amount_value), Some(token_value)) =
868                    (data.get("amount_out"), data.get("to_token"))
869                {
870                    if let (Some(amount), Some(token_str)) = (
871                        Self::parse_amount_simple(amount_value),
872                        token_value.as_str(),
873                    ) {
874                        result.push((token_str.to_string(), amount));
875                    }
876                }
877                let output_combos = [
878                    ("amount_y_out", "token_y"),
879                    ("output_amount", "output_token"),
880                    ("amount1_out", "token1"),
881                ];
882                for (amount_field, token_field) in &output_combos {
883                    if let (Some(amount_value), Some(token_value)) =
884                        (data.get(*amount_field), data.get(*token_field))
885                    {
886                        if let (Some(amount), Some(token_str)) = (
887                            Self::parse_amount_simple(amount_value),
888                            token_value.as_str(),
889                        ) {
890                            result.push((token_str.to_string(), amount));
891                        }
892                    }
893                }
894            }
895            if event.r#type.contains("fungible_asset::Deposit") {
896                if let Some(amount_value) = data.get("amount") {
897                    if let Some(amount) = Self::parse_amount_simple(amount_value) {
898                        let token_type = Self::infer_token_from_event_type(event);
899                        if let Some(token) = token_type {
900                            result.push((token.clone(), amount));
901                        }
902                    }
903                }
904            }
905        }
906        result
907    }
908
909    fn extract_spent_from_event(event: &Event) -> Vec<(String, u64)> {
910        let mut result = Vec::new();
911        if let serde_json::Value::Object(data) = &event.data {
912            if event.r#type.contains("Swap") {
913                if let (Some(amount_value), Some(token_value)) =
914                    (data.get("amount_in"), data.get("from_token"))
915                {
916                    if let (Some(amount), Some(token_str)) = (
917                        Self::parse_amount_simple(amount_value),
918                        token_value.as_str(),
919                    ) {
920                        result.push((token_str.to_string(), amount));
921                    }
922                }
923                let input_combos = [
924                    ("amount_x_in", "token_x"),
925                    ("amount0_in", "token0"),
926                    ("input_amount", "input_token"),
927                ];
928                for (amount_field, token_field) in &input_combos {
929                    if let (Some(amount_value), Some(token_value)) =
930                        (data.get(*amount_field), data.get(*token_field))
931                    {
932                        if let (Some(amount), Some(token_str)) = (
933                            Self::parse_amount_simple(amount_value),
934                            token_value.as_str(),
935                        ) {
936                            result.push((token_str.to_string(), amount));
937                        }
938                    }
939                }
940            }
941            if event.r#type.contains("fungible_asset::Withdraw") {
942                if let Some(amount_value) = data.get("amount") {
943                    if let Some(amount) = Self::parse_amount_simple(amount_value) {
944                        let token_type = Self::infer_token_from_event_type(event);
945                        if let Some(token) = token_type {
946                            result.push((token.clone(), amount));
947                        }
948                    }
949                }
950            }
951        }
952        result
953    }
954
955    fn infer_token_from_event_type(event: &Event) -> Option<String> {
956        let event_type = &event.r#type;
957        if event_type.contains("aptos_coin") {
958            Some("0x1::aptos_coin::AptosCoin".to_string())
959        } else if event_type.contains("usdt") || event_type.contains("USDt") {
960            Some("0x1::usdt::USDT".to_string())
961        } else if event_type.contains("EchoCoin002") {
962            Some("0xe4ccb6d39136469f376242c31b34d10515c8eaaa38092f804db8e08a8f53c5b2::assets_v1::EchoCoin002".to_string())
963        } else if event_type
964            .contains("0x2ebb2ccac5e027a87fa0e2e5f656a3a4238d6a48d93ec9b610d570fc0aa0df12")
965        {
966            Some("0x2ebb2ccac5e027a87fa0e2e5f656a3a4238d6a48d93ec9b610d570fc0aa0df12".to_string())
967        } else if event_type
968            .contains("0x357b0b74bc833e95a115ad22604854d6b0fca151cecd94111770e5d6ffc9dc2b")
969        {
970            Some("0x357b0b74bc833e95a115ad22604854d6b0fca151cecd94111770e5d6ffc9dc2b".to_string())
971        } else {
972            None
973        }
974    }
975
976    pub fn get_spent_token(&self) -> Option<(String, u64)> {
977        if !self.success {
978            return None;
979        }
980        for event in self.events.iter().rev() {
981            if event.r#type.contains("Swap") {
982                if let serde_json::Value::Object(data) = &event.data {
983                    let input_pairs = [
984                        ("amount_in", "from_token"),
985                        ("amount_x_in", "token_x"),
986                        ("amount0_in", "token0"),
987                        ("input_amount", "input_token"),
988                        ("amount", "coin_type"),
989                    ];
990                    for (amount_field, token_field) in &input_pairs {
991                        if let (Some(amount_value), Some(token_value)) =
992                            (data.get(*amount_field), data.get(*token_field))
993                        {
994                            if let (Some(amount), Some(token_str)) = (
995                                Self::parse_amount_simple(amount_value),
996                                Self::extract_token_string(token_value),
997                            ) {
998                                if amount > 0 {
999                                    return Some((token_str, amount));
1000                                }
1001                            }
1002                        }
1003                    }
1004                }
1005            }
1006        }
1007
1008        None
1009    }
1010
1011    pub fn get_received_token(&self) -> Option<(String, u64)> {
1012        if !self.success {
1013            return None;
1014        }
1015        for event in self.events.iter().rev() {
1016            if event.r#type.contains("Swap") {
1017                if let serde_json::Value::Object(data) = &event.data {
1018                    let output_pairs = [
1019                        ("amount_out", "to_token"),
1020                        ("amount_y_out", "token_y"),
1021                        ("amount1_out", "token1"),
1022                        ("output_amount", "output_token"),
1023                        ("amount", "coin_type"),
1024                    ];
1025                    for (amount_field, token_field) in &output_pairs {
1026                        if let (Some(amount_value), Some(token_value)) =
1027                            (data.get(*amount_field), data.get(*token_field))
1028                        {
1029                            if let (Some(amount), Some(token_str)) = (
1030                                Self::parse_amount_simple(amount_value),
1031                                Self::extract_token_string(token_value),
1032                            ) {
1033                                if amount > 0 {
1034                                    return Some((token_str, amount));
1035                                }
1036                            }
1037                        }
1038                    }
1039                }
1040            }
1041        }
1042        None
1043    }
1044
1045    fn guess_decimals_from_amount(amount: u64) -> u8 {
1046        let amount_str = amount.to_string();
1047        let len = amount_str.len();
1048        if len > 6 && amount_str.ends_with("000000") {
1049            return 6;
1050        }
1051        if len > 8 && amount_str.ends_with("00000000") {
1052            return 8;
1053        }
1054        if amount > 1_000_000_000_000 {
1055            return 6;
1056        } else if amount > 10_000_000 && amount < 100_000_000_000 {
1057            return 8;
1058        } else {
1059            return 6;
1060        }
1061    }
1062
1063    fn extract_token_string(value: &serde_json::Value) -> Option<String> {
1064        match value {
1065            serde_json::Value::String(s) => match s.as_str() {
1066                "0xa" => Some("0x1::aptos_coin::AptosCoin".to_string()),
1067                _ => Some(s.clone()),
1068            },
1069            serde_json::Value::Object(obj) => {
1070                for field in ["inner", "value", "address", "token"] {
1071                    if let Some(inner_value) = obj.get(field) {
1072                        if let Some(result) = Self::extract_token_string(inner_value) {
1073                            return Some(result);
1074                        }
1075                    }
1076                }
1077                None
1078            }
1079            _ => None,
1080        }
1081    }
1082
1083    pub fn get_spent_token_eth(&self) -> Option<(String, f64)> {
1084        self.get_spent_token().map(|(token, amount)| {
1085            let decimals = Self::guess_decimals_from_amount(amount);
1086            let decimal_amount = amount as f64 / 10_u64.pow(decimals as u32) as f64;
1087            (token, decimal_amount)
1088        })
1089    }
1090
1091    pub fn get_received_token_eth(&self) -> Option<(String, f64)> {
1092        self.get_received_token().map(|(token, amount)| {
1093            let decimals = Self::guess_decimals_from_amount(amount);
1094            let decimal_amount = amount as f64 / 10_u64.pow(decimals as u32) as f64;
1095            (token, decimal_amount)
1096        })
1097    }
1098
1099    fn parse_amount_simple(value: &serde_json::Value) -> Option<u64> {
1100        if let Some(s) = value.as_str() {
1101            if let Ok(n) = s.parse::<u64>() {
1102                return Some(n);
1103            }
1104        }
1105        if let Some(n) = value.as_u64() {
1106            return Some(n);
1107        }
1108        if let Some(n) = value.as_i64() {
1109            if n >= 0 {
1110                return Some(n as u64);
1111            }
1112        }
1113        None
1114    }
1115
1116    pub fn getDirection(&self) -> String {
1117        match (self.get_spent_token_eth(), self.get_received_token_eth()) {
1118            (Some((spent_token, _)), Some((received_token, _))) => {
1119                if spent_token.contains("EchoCoin002") && received_token.contains("aptos_coin") {
1120                    "BUY".to_string()
1121                } else if spent_token.contains("aptos_coin")
1122                    && received_token.contains("EchoCoin002")
1123                {
1124                    "SELL".to_string()
1125                } else {
1126                    "SWAP".to_string()
1127                }
1128            }
1129            _ => "TRANSFER".to_string(),
1130        }
1131    }
1132
1133    fn get_decimals_for_token(token: &str) -> u8 {
1134        if token.contains("EchoCoin002")
1135            || token.contains("0x9da434d9b873b5159e8eeed70202ad22dc075867a7793234fbc981b63e119")
1136        {
1137            6
1138        } else if token.contains("aptos_coin") || token == "0xa" {
1139            8
1140        } else if token
1141            .contains("0x2ebb2ccac5e027a87fa0e2e5f656a3a4238d6a48d93ec9b610d570fc0aa0df12")
1142        {
1143            8
1144        } else if token
1145            .contains("0x357b0b74bc833e95a115ad22604854d6b0fca151cecd94111770e5d6ffc9dc2b")
1146        {
1147            6
1148        } else {
1149            8
1150        }
1151    }
1152
1153    pub fn calculate_all_token_balances(&self) {
1154        let mut spent_map: HashMap<String, u64> = HashMap::new();
1155        let mut received_map: HashMap<String, u64> = HashMap::new();
1156        for event in &self.events {
1157            let spent = Self::extract_spent_from_event(event);
1158            for (token, amount) in spent {
1159                *spent_map.entry(token).or_insert(0) += amount;
1160            }
1161        }
1162        for event in &self.events {
1163            let received = Self::extract_received_from_event(event);
1164            for (token, amount) in received {
1165                *received_map.entry(token).or_insert(0) += amount;
1166            }
1167        }
1168        for (token, total) in &spent_map {
1169            let decimals = Self::get_decimals_for_token(token);
1170        }
1171        for (token, total) in &received_map {
1172            let decimals = Self::get_decimals_for_token(token);
1173        }
1174        let all_tokens: HashSet<_> = spent_map.keys().chain(received_map.keys()).collect();
1175        for token in all_tokens {
1176            let spent = spent_map.get(token).copied().unwrap_or(0);
1177            let received = received_map.get(token).copied().unwrap_or(0);
1178            let net = received as i128 - spent as i128;
1179            if net != 0 {
1180                let decimals = Self::get_decimals_for_token(token);
1181            }
1182        }
1183    }
1184
1185    pub fn get_liquidity_pool_addresses(&self) -> Vec<String> {
1186        let mut pool_addresses = Vec::new();
1187        for event in &self.events {
1188            let event_type = &event.r#type;
1189            if event_type.contains("Pool") || event_type.contains("Swap") {
1190                if let guid = &event.guid {
1191                    pool_addresses.push(guid.account_address.clone());
1192                }
1193                if let serde_json::Value::Object(data) = &event.data {
1194                    let possible_fields = [
1195                        "pool_address",
1196                        "pool",
1197                        "pair",
1198                        "liquidity_pool",
1199                        "address",
1200                        "contract_address",
1201                        "dex_address",
1202                    ];
1203                    for field in &possible_fields {
1204                        if let Some(addr_value) = data.get(*field) {
1205                            if let Some(addr_str) = addr_value.as_str() {
1206                                pool_addresses.push(addr_str.to_string());
1207                                break;
1208                            }
1209                        }
1210                    }
1211                }
1212            }
1213        }
1214        pool_addresses.sort();
1215        pool_addresses.dedup();
1216        pool_addresses
1217    }
1218
1219    pub fn get_dex_names(&self) -> Vec<String> {
1220        let mut dex_names = Vec::new();
1221        if let TransactionType::UserTransaction(user_txn) = &self.transaction_type {
1222            let function = &user_txn.payload.function;
1223            if function.contains("panora_swap") {
1224                dex_names.push("Panora Exchange".to_string());
1225            }
1226            if function.contains("pancake") {
1227                dex_names.push("PancakeSwap".to_string());
1228            }
1229            if function.contains("hyperion") {
1230                dex_names.push("Hyperion".to_string());
1231            }
1232            if function.contains("tapp") {
1233                dex_names.push("Tapp Exchange".to_string());
1234            }
1235            if function.contains("cellana") {
1236                dex_names.push("Cellana Finance".to_string());
1237            }
1238        }
1239        for event in &self.events {
1240            let event_type = &event.r#type;
1241
1242            if event_type.contains("panora") && !dex_names.contains(&"Panora Exchange".to_string())
1243            {
1244                dex_names.push("Panora Exchange".to_string());
1245            }
1246            if event_type.contains("pancake") && !dex_names.contains(&"PancakeSwap".to_string()) {
1247                dex_names.push("PancakeSwap".to_string());
1248            }
1249            if event_type.contains("hyperion") && !dex_names.contains(&"Hyperion".to_string()) {
1250                dex_names.push("Hyperion".to_string());
1251            }
1252            if event_type.contains("tapp") && !dex_names.contains(&"Tapp Exchange".to_string()) {
1253                dex_names.push("Tapp Exchange".to_string());
1254            }
1255            if event_type.contains("cellana") && !dex_names.contains(&"Cellana Finance".to_string())
1256            {
1257                dex_names.push("Cellana Finance".to_string());
1258            }
1259            if let serde_json::Value::Object(data) = &event.data {
1260                let dex_fields = ["dex", "exchange", "platform", "protocol"];
1261                for field in &dex_fields {
1262                    if let Some(dex_value) = data.get(*field) {
1263                        if let Some(dex_str) = dex_value.as_str() {
1264                            let dex_name = dex_str.to_string();
1265                            if !dex_names.contains(&dex_name) {
1266                                dex_names.push(dex_name);
1267                            }
1268                        }
1269                    }
1270                }
1271            }
1272        }
1273        if dex_names.is_empty() {
1274            let pools = self.get_liquidity_pool_addresses();
1275            for pool in pools {
1276                if pool.contains("0x1c3206") {
1277                    dex_names.push("Panora Exchange".to_string());
1278                } else if pool.contains("0x2788f4") {
1279                    dex_names.push("Hyperion".to_string());
1280                } else if pool.contains("0x85d333") {
1281                    dex_names.push("Tapp Exchange".to_string());
1282                } else if pool.contains("0xd18e39") {
1283                    dex_names.push("Cellana Finance".to_string());
1284                }
1285            }
1286        }
1287        dex_names.sort();
1288        dex_names.dedup();
1289        dex_names
1290    }
1291}
1292
1293#[cfg(test)]
1294mod tests {
1295    use crate::AptosType;
1296
1297    use super::*;
1298    use std::sync::Arc;
1299
1300    #[tokio::test]
1301    async fn test_get_specific_transaction() {
1302        let client = Aptos::new(AptosType::Mainnet);
1303        let known_tx_hash = "0x280a3e0c7e2ab02de2f8052441464fd8b351804c9d336ec988d75b59446ecfdc";
1304        let result = client.get_transaction_info_by_hash(known_tx_hash).await;
1305        match result {
1306            Ok(tx) => {
1307                println!("Spent {:?}", tx.get_spent_token_eth());
1308                println!("Received {:?}", tx.get_received_token_eth());
1309                println!("Liquidity Pool {:?}", tx.get_liquidity_pool_addresses());
1310            }
1311            Err(e) => {
1312                println!("❌ error: {}", e);
1313            }
1314        }
1315    }
1316}