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 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 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 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 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 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 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 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 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 let message_to_sign = serialize_transaction_and_sign(&raw_txn)?;
137 match wallet.sign(&message_to_sign) {
139 Ok(signature_bytes) => {
140 match Trade::create_signed_transaction_tx(
142 Arc::clone(&wallet),
143 raw_txn,
144 signature_bytes,
145 ) {
146 Ok(signed_txn) => {
147 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 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 let current_timestamp = std::time::SystemTime::now()
184 .duration_since(std::time::UNIX_EPOCH)
185 .unwrap()
186 .as_secs();
187 let expiration_timestamp = current_timestamp + expiration_secs;
189 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 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 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 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 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 pub async fn get_transactions_by_recipient_sender(
396 client: Arc<Aptos>,
397 address_a: &str, address_b: &str, 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 fn transaction_involves_address(transaction: &TransactionInfo, address: &str) -> bool {
414 match &transaction.transaction_type {
415 TransactionType::UserTransaction(user_txn) => {
416 if user_txn.sender == address {
418 return true;
419 }
420 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 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 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 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 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 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 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 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 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
551pub struct BatchTradeHandle;
553
554impl BatchTradeHandle {
555 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 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#[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 pub fn is_successful(&self) -> bool {
836 self.success
837 }
838
839 pub fn get_timestamp(&self) -> Option<u64> {
841 self.timestamp.clone().unwrap_or(0.to_string()).parse().ok()
842 }
843
844 pub fn get_gas_used(&self) -> Option<u64> {
846 self.gas_used.parse().ok()
847 }
848
849 pub fn is_user_transaction(&self) -> bool {
851 matches!(self.transaction_type, TransactionType::UserTransaction(_))
852 }
853
854 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}