1use std::collections::HashMap;
4use std::ops::Not;
5
6use hiero_sdk_proto::services;
7use hiero_sdk_proto::services::crypto_service_client::CryptoServiceClient;
8use tonic::transport::Channel;
9
10use crate::hooks::{
11 FungibleHookCall,
12 FungibleHookType,
13 NftHookCall,
14 NftHookType,
15};
16use crate::ledger_id::RefLedgerId;
17use crate::protobuf::FromProtobuf;
18use crate::transaction::{
19 AnyTransactionData,
20 ChunkInfo,
21 ToSchedulableTransactionDataProtobuf,
22 ToTransactionDataProtobuf,
23 TransactionData,
24 TransactionExecute,
25};
26use crate::{
27 AccountId,
28 BoxGrpcFuture,
29 Error,
30 Hbar,
31 NftId,
32 ToProtobuf,
33 TokenId,
34 TokenNftTransfer,
35 Transaction,
36 ValidateChecksums,
37};
38
39pub type TransferTransaction = Transaction<TransferTransactionData>;
50
51#[derive(Debug, Clone, Default)]
52#[cfg_attr(test, derive(Eq, PartialEq))]
53pub struct TransferTransactionData {
54 transfers: Vec<Transfer>,
55 token_transfers: Vec<TokenTransfer>,
56}
57
58#[derive(Debug, Clone)]
59#[cfg_attr(test, derive(Eq, PartialEq))]
60pub(crate) struct Transfer {
61 pub account_id: AccountId,
63
64 pub amount: i64,
66
67 pub is_approval: bool,
69
70 pub hook_call: Option<FungibleHookCall>,
72}
73
74#[derive(Debug, Clone)]
75#[cfg_attr(test, derive(Eq, PartialEq))]
76pub(crate) struct TokenTransfer {
77 pub token_id: TokenId,
78
79 pub transfers: Vec<Transfer>,
80
81 pub nft_transfers: Vec<TokenNftTransfer>,
82
83 pub expected_decimals: Option<u32>,
84}
85
86impl TransferTransaction {
87 fn _hbar_transfer(
88 &mut self,
89 account_id: AccountId,
90 amount: Hbar,
91 approved: bool,
92 hook_call: Option<FungibleHookCall>,
93 ) -> &mut Self {
94 self.data_mut().transfers.push(Transfer {
95 account_id,
96 amount: amount.to_tinybars(),
97 is_approval: approved,
98 hook_call,
99 });
100
101 self
102 }
103
104 pub fn hbar_transfer(&mut self, account_id: AccountId, amount: Hbar) -> &mut Self {
106 self._hbar_transfer(account_id, amount, false, None)
107 }
108
109 pub fn approved_hbar_transfer(&mut self, account_id: AccountId, amount: Hbar) -> &mut Self {
111 self._hbar_transfer(account_id, amount, true, None)
112 }
113
114 pub fn get_hbar_transfers(&self) -> HashMap<AccountId, Hbar> {
116 self.data()
117 .transfers
118 .iter()
119 .map(|it| (it.account_id, Hbar::from_tinybars(it.amount)))
120 .collect()
121 }
122
123 fn _token_transfer(
124 &mut self,
125 token_id: TokenId,
126 account_id: AccountId,
127 amount: i64,
128 approved: bool,
129 expected_decimals: Option<u32>,
130 hook_call: Option<FungibleHookCall>,
131 ) -> &mut Self {
132 let transfer = Transfer { account_id, amount, is_approval: approved, hook_call };
133 let data = self.data_mut();
134
135 if let Some(tt) = data.token_transfers.iter_mut().find(|tt| tt.token_id == token_id) {
137 if let Some(existing_transfer) = tt
139 .transfers
140 .iter_mut()
141 .find(|t| t.account_id == account_id && t.is_approval == approved)
142 {
143 existing_transfer.amount += amount;
145 tt.expected_decimals = expected_decimals;
146 } else {
147 tt.transfers.push(transfer.clone());
149 tt.expected_decimals = expected_decimals;
150 }
151 } else {
152 data.token_transfers.push(TokenTransfer {
154 token_id,
155 expected_decimals,
156 nft_transfers: Vec::new(),
157 transfers: vec![transfer],
158 });
159 }
160
161 self
162 }
163
164 pub fn token_transfer(
168 &mut self,
169 token_id: TokenId,
170 account_id: AccountId,
171 amount: i64,
172 ) -> &mut Self {
173 self._token_transfer(token_id, account_id, amount, false, None, None)
174 }
175
176 pub fn approved_token_transfer(
180 &mut self,
181 token_id: TokenId,
182 account_id: AccountId,
183 amount: i64,
184 ) -> &mut Self {
185 self._token_transfer(token_id, account_id, amount, true, None, None)
186 }
187
188 pub fn token_transfer_with_decimals(
194 &mut self,
195 token_id: TokenId,
196 account_id: AccountId,
197 amount: i64,
198 expected_decimals: u32,
199 ) -> &mut Self {
200 self._token_transfer(token_id, account_id, amount, false, Some(expected_decimals), None)
201 }
202
203 pub fn approved_token_transfer_with_decimals(
208 &mut self,
209 token_id: TokenId,
210 account_id: AccountId,
211 amount: i64,
212 expected_decimals: u32,
213 ) -> &mut Self {
214 self._token_transfer(token_id, account_id, amount, true, Some(expected_decimals), None)
215 }
216
217 pub fn get_token_transfers(&self) -> HashMap<TokenId, HashMap<AccountId, i64>> {
219 use std::collections::hash_map::Entry;
220
221 self.data().token_transfers.iter().fold(
223 HashMap::with_capacity(self.data().token_transfers.len()),
224 |mut map, transfer| {
225 let iter = transfer.transfers.iter().map(|it| (it.account_id, it.amount));
226 match map.entry(transfer.token_id) {
227 Entry::Occupied(mut it) => it.get_mut().extend(iter),
228 Entry::Vacant(it) => {
229 it.insert(iter.collect());
230 }
231 }
232
233 map
234 },
235 )
236 }
237
238 pub fn get_token_decimals(&self) -> HashMap<TokenId, u32> {
240 self.data()
241 .token_transfers
242 .iter()
243 .filter_map(|it| it.expected_decimals.map(|decimals| (it.token_id, decimals)))
244 .collect()
245 }
246
247 fn _nft_transfer(
248 &mut self,
249 nft_id: NftId,
250 sender_account_id: AccountId,
251 receiver_account_id: AccountId,
252 approved: bool,
253 sender_hook_call: Option<NftHookCall>,
254 receiver_hook_call: Option<NftHookCall>,
255 ) -> &mut Self {
256 let NftId { token_id, serial } = nft_id;
257 let transfer = TokenNftTransfer {
258 token_id,
259 serial,
260 sender: sender_account_id,
261 receiver: receiver_account_id,
262 is_approved: approved,
263 sender_hook_call,
264 receiver_hook_call,
265 };
266
267 let data = self.data_mut();
268
269 if let Some(tt) = data.token_transfers.iter_mut().find(|tt| tt.token_id == token_id) {
270 tt.nft_transfers.push(transfer);
271 } else {
272 data.token_transfers.push(TokenTransfer {
273 token_id,
274 expected_decimals: None,
275 transfers: Vec::new(),
276 nft_transfers: vec![transfer],
277 });
278 }
279
280 self
281 }
282
283 pub fn approved_nft_transfer(
285 &mut self,
286 nft_id: impl Into<NftId>,
287 sender_account_id: AccountId,
288 receiver_account_id: AccountId,
289 ) -> &mut Self {
290 self._nft_transfer(nft_id.into(), sender_account_id, receiver_account_id, true, None, None)
291 }
292
293 pub fn nft_transfer(
295 &mut self,
296 nft_id: impl Into<NftId>,
297 sender_account_id: AccountId,
298 receiver_account_id: AccountId,
299 ) -> &mut Self {
300 self._nft_transfer(nft_id.into(), sender_account_id, receiver_account_id, false, None, None)
301 }
302
303 pub fn get_nft_transfers(&self) -> HashMap<TokenId, Vec<TokenNftTransfer>> {
305 self.data()
306 .token_transfers
307 .iter()
308 .map(|it| (it.token_id, it.nft_transfers.clone()))
309 .collect()
310 }
311
312 pub fn add_hbar_transfer_with_hook(
314 &mut self,
315 account_id: AccountId,
316 amount: Hbar,
317 hook_call: FungibleHookCall,
318 ) -> &mut Self {
319 self._hbar_transfer(account_id, amount, false, Some(hook_call))
320 }
321
322 pub fn add_token_transfer_with_hook(
324 &mut self,
325 token_id: TokenId,
326 account_id: AccountId,
327 amount: i64,
328 hook_call: FungibleHookCall,
329 ) -> &mut Self {
330 self._token_transfer(token_id, account_id, amount, false, None, Some(hook_call))
331 }
332
333 pub fn add_nft_transfer_with_hook(
335 &mut self,
336 nft_id: impl Into<NftId>,
337 sender: AccountId,
338 receiver: AccountId,
339 sender_hook_call: Option<NftHookCall>,
340 receiver_hook_call: Option<NftHookCall>,
341 ) -> &mut Self {
342 self._nft_transfer(
343 nft_id.into(),
344 sender,
345 receiver,
346 false,
347 sender_hook_call,
348 receiver_hook_call,
349 )
350 }
351}
352
353impl TransactionExecute for TransferTransactionData {
354 fn execute(
356 &self,
357 channel: Channel,
358 request: services::Transaction,
359 ) -> BoxGrpcFuture<'_, services::TransactionResponse> {
360 Box::pin(async { CryptoServiceClient::new(channel).crypto_transfer(request).await })
361 }
362}
363
364impl TransactionData for TransferTransactionData {}
365
366impl ValidateChecksums for TransferTransactionData {
367 fn validate_checksums(&self, ledger_id: &RefLedgerId) -> Result<(), Error> {
368 for transfer in &self.transfers {
369 transfer.account_id.validate_checksums(ledger_id)?;
370 }
371 for token_transfer in &self.token_transfers {
372 token_transfer.token_id.validate_checksums(ledger_id)?;
373 for transfer in &token_transfer.transfers {
374 transfer.account_id.validate_checksums(ledger_id)?;
375 }
376 for nft_transfer in &token_transfer.nft_transfers {
377 nft_transfer.sender.validate_checksums(ledger_id)?;
378 nft_transfer.receiver.validate_checksums(ledger_id)?;
379 }
380 }
381 Ok(())
382 }
383}
384
385impl FromProtobuf<services::AccountAmount> for Transfer {
386 fn from_protobuf(pb: services::AccountAmount) -> crate::Result<Self> {
387 let hook_call = match pb.hook_call {
388 Some(services::account_amount::HookCall::PreTxAllowanceHook(hook)) => {
389 Some(FungibleHookCall::from_protobuf_with_type(
390 hook,
391 FungibleHookType::PreTxAllowanceHook,
392 )?)
393 }
394 Some(services::account_amount::HookCall::PrePostTxAllowanceHook(hook)) => {
395 Some(FungibleHookCall::from_protobuf_with_type(
396 hook,
397 FungibleHookType::PrePostTxAllowanceHook,
398 )?)
399 }
400 None => None,
401 };
402
403 Ok(Self {
404 account_id: AccountId::from_protobuf(pb_getf!(pb, account_id)?)?,
405 amount: pb.amount,
406 is_approval: pb.is_approval,
407 hook_call,
408 })
409 }
410}
411
412impl ToProtobuf for Transfer {
413 type Protobuf = services::AccountAmount;
414
415 fn to_protobuf(&self) -> Self::Protobuf {
416 let hook_call = self.hook_call.as_ref().map(|hook| match hook.hook_type {
417 FungibleHookType::PreTxAllowanceHook => {
418 services::account_amount::HookCall::PreTxAllowanceHook(hook.to_protobuf())
419 }
420 FungibleHookType::PrePostTxAllowanceHook => {
421 services::account_amount::HookCall::PrePostTxAllowanceHook(hook.to_protobuf())
422 }
423 });
424
425 services::AccountAmount {
426 account_id: Some(self.account_id.to_protobuf()),
427 amount: self.amount,
428 is_approval: self.is_approval,
429 hook_call,
430 }
431 }
432}
433
434impl FromProtobuf<services::TokenTransferList> for TokenTransfer {
435 fn from_protobuf(pb: services::TokenTransferList) -> crate::Result<Self> {
436 let token_id = TokenId::from_protobuf(pb_getf!(pb, token)?)?;
437
438 Ok(Self {
439 token_id,
440 transfers: Vec::from_protobuf(pb.transfers)?,
441 nft_transfers: pb
442 .nft_transfers
443 .into_iter()
444 .map(|pb| TokenNftTransfer::from_protobuf(pb, token_id))
445 .collect::<Result<Vec<_>, _>>()?,
446 expected_decimals: pb.expected_decimals,
447 })
448 }
449}
450
451impl ToProtobuf for TokenTransfer {
452 type Protobuf = services::TokenTransferList;
453
454 fn to_protobuf(&self) -> Self::Protobuf {
455 let transfers = self.transfers.to_protobuf();
456 let nft_transfers = self.nft_transfers.to_protobuf();
457
458 services::TokenTransferList {
459 token: Some(self.token_id.to_protobuf()),
460 transfers,
461 nft_transfers,
462 expected_decimals: self.expected_decimals,
463 }
464 }
465}
466
467impl ToProtobuf for TokenNftTransfer {
468 type Protobuf = services::NftTransfer;
469
470 fn to_protobuf(&self) -> Self::Protobuf {
471 let sender_allowance_hook_call = self.sender_hook_call.as_ref().map(|hook| {
473 match hook.hook_type {
474 NftHookType::PreHookSender => {
475 services::nft_transfer::SenderAllowanceHookCall::PreTxSenderAllowanceHook(
476 hook.to_protobuf(),
477 )
478 }
479 NftHookType::PrePostHookSender => {
480 services::nft_transfer::SenderAllowanceHookCall::PrePostTxSenderAllowanceHook(
481 hook.to_protobuf(),
482 )
483 }
484 _ => {
485 services::nft_transfer::SenderAllowanceHookCall::PreTxSenderAllowanceHook(
487 hook.to_protobuf(),
488 )
489 }
490 }
491 });
492
493 let receiver_allowance_hook_call = self.receiver_hook_call.as_ref().map(|hook| {
495 match hook.hook_type {
496 NftHookType::PreHookReceiver => {
497 services::nft_transfer::ReceiverAllowanceHookCall::PreTxReceiverAllowanceHook(
498 hook.to_protobuf(),
499 )
500 }
501 NftHookType::PrePostHookReceiver => {
502 services::nft_transfer::ReceiverAllowanceHookCall::PrePostTxReceiverAllowanceHook(
503 hook.to_protobuf(),
504 )
505 }
506 _ => {
507 services::nft_transfer::ReceiverAllowanceHookCall::PreTxReceiverAllowanceHook(
509 hook.to_protobuf(),
510 )
511 }
512 }
513 });
514
515 services::NftTransfer {
516 sender_account_id: Some(self.sender.to_protobuf()),
517 receiver_account_id: Some(self.receiver.to_protobuf()),
518 serial_number: self.serial as i64,
519 is_approval: self.is_approved,
520 sender_allowance_hook_call,
521 receiver_allowance_hook_call,
522 }
523 }
524}
525
526impl ToTransactionDataProtobuf for TransferTransactionData {
527 fn to_transaction_data_protobuf(
528 &self,
529 chunk_info: &ChunkInfo,
530 ) -> services::transaction_body::Data {
531 let _ = chunk_info.assert_single_transaction();
532
533 services::transaction_body::Data::CryptoTransfer(self.to_protobuf())
534 }
535}
536
537impl ToSchedulableTransactionDataProtobuf for TransferTransactionData {
538 fn to_schedulable_transaction_data_protobuf(
539 &self,
540 ) -> services::schedulable_transaction_body::Data {
541 services::schedulable_transaction_body::Data::CryptoTransfer(self.to_protobuf())
542 }
543}
544
545impl From<TransferTransactionData> for AnyTransactionData {
546 fn from(transaction: TransferTransactionData) -> Self {
547 Self::Transfer(transaction)
548 }
549}
550
551impl FromProtobuf<services::CryptoTransferTransactionBody> for TransferTransactionData {
552 fn from_protobuf(pb: services::CryptoTransferTransactionBody) -> crate::Result<Self> {
553 let transfers = pb.transfers.map(|it| it.account_amounts);
554 let transfers = Option::from_protobuf(transfers)?.unwrap_or_default();
555
556 Ok(Self { transfers, token_transfers: Vec::from_protobuf(pb.token_transfers)? })
557 }
558}
559
560impl ToProtobuf for TransferTransactionData {
561 type Protobuf = services::CryptoTransferTransactionBody;
562
563 fn to_protobuf(&self) -> Self::Protobuf {
564 let transfers = self
565 .transfers
566 .is_empty()
567 .not()
568 .then(|| services::TransferList { account_amounts: self.transfers.to_protobuf() });
569
570 let token_transfers = self.token_transfers.to_protobuf();
571
572 services::CryptoTransferTransactionBody { transfers, token_transfers }
573 }
574}
575
576#[cfg(test)]
577mod tests {
578 use expect_test::expect;
579
580 use crate::transaction::test_helpers::{
581 check_body,
582 transaction_body,
583 };
584 use crate::{
585 AccountId,
586 AnyTransaction,
587 Hbar,
588 TokenId,
589 TransferTransaction,
590 };
591
592 fn make_transaction() -> TransferTransaction {
593 let mut tx = TransferTransaction::new_for_tests();
594
595 tx.hbar_transfer(AccountId::new(0, 0, 5008), Hbar::from_tinybars(400))
596 .hbar_transfer(AccountId::new(0, 0, 5006), Hbar::from_tinybars(800).negated())
597 .approved_hbar_transfer(AccountId::new(0, 0, 5007), Hbar::from_tinybars(400))
598 .token_transfer(TokenId::new(0, 0, 5), AccountId::new(0, 0, 5008), 400)
599 .token_transfer_with_decimals(
600 TokenId::new(0, 0, 5),
601 AccountId::new(0, 0, 5006),
602 -800,
603 3,
604 )
605 .token_transfer_with_decimals(TokenId::new(0, 0, 5), AccountId::new(0, 0, 5007), 400, 3)
606 .token_transfer(TokenId::new(0, 0, 4), AccountId::new(0, 0, 5008), 1)
607 .approved_token_transfer(TokenId::new(0, 0, 4), AccountId::new(0, 0, 5006), -1)
608 .nft_transfer(
609 TokenId::new(0, 0, 3).nft(2),
610 AccountId::new(0, 0, 5008),
611 AccountId::new(0, 0, 5007),
612 )
613 .approved_nft_transfer(
614 TokenId::new(0, 0, 3).nft(1),
615 AccountId::new(0, 0, 5008),
616 AccountId::new(0, 0, 5007),
617 )
618 .nft_transfer(
619 TokenId::new(0, 0, 3).nft(3),
620 AccountId::new(0, 0, 5008),
621 AccountId::new(0, 0, 5006),
622 )
623 .nft_transfer(
624 TokenId::new(0, 0, 3).nft(4),
625 AccountId::new(0, 0, 5007),
626 AccountId::new(0, 0, 5006),
627 )
628 .nft_transfer(
629 TokenId::new(0, 0, 2).nft(4),
630 AccountId::new(0, 0, 5007),
631 AccountId::new(0, 0, 5006),
632 )
633 .freeze()
634 .unwrap();
635
636 tx
637 }
638
639 #[test]
640 fn serialize() {
641 let tx = make_transaction();
642
643 let tx = transaction_body(tx);
644
645 let tx = check_body(tx);
646
647 expect![[r#"
648 CryptoTransfer(
649 CryptoTransferTransactionBody {
650 transfers: Some(
651 TransferList {
652 account_amounts: [
653 AccountAmount {
654 account_id: Some(
655 AccountId {
656 shard_num: 0,
657 realm_num: 0,
658 account: Some(
659 AccountNum(
660 5008,
661 ),
662 ),
663 },
664 ),
665 amount: 400,
666 is_approval: false,
667 hook_call: None,
668 },
669 AccountAmount {
670 account_id: Some(
671 AccountId {
672 shard_num: 0,
673 realm_num: 0,
674 account: Some(
675 AccountNum(
676 5006,
677 ),
678 ),
679 },
680 ),
681 amount: -800,
682 is_approval: false,
683 hook_call: None,
684 },
685 AccountAmount {
686 account_id: Some(
687 AccountId {
688 shard_num: 0,
689 realm_num: 0,
690 account: Some(
691 AccountNum(
692 5007,
693 ),
694 ),
695 },
696 ),
697 amount: 400,
698 is_approval: true,
699 hook_call: None,
700 },
701 ],
702 },
703 ),
704 token_transfers: [
705 TokenTransferList {
706 token: Some(
707 TokenId {
708 shard_num: 0,
709 realm_num: 0,
710 token_num: 5,
711 },
712 ),
713 transfers: [
714 AccountAmount {
715 account_id: Some(
716 AccountId {
717 shard_num: 0,
718 realm_num: 0,
719 account: Some(
720 AccountNum(
721 5008,
722 ),
723 ),
724 },
725 ),
726 amount: 400,
727 is_approval: false,
728 hook_call: None,
729 },
730 AccountAmount {
731 account_id: Some(
732 AccountId {
733 shard_num: 0,
734 realm_num: 0,
735 account: Some(
736 AccountNum(
737 5006,
738 ),
739 ),
740 },
741 ),
742 amount: -800,
743 is_approval: false,
744 hook_call: None,
745 },
746 AccountAmount {
747 account_id: Some(
748 AccountId {
749 shard_num: 0,
750 realm_num: 0,
751 account: Some(
752 AccountNum(
753 5007,
754 ),
755 ),
756 },
757 ),
758 amount: 400,
759 is_approval: false,
760 hook_call: None,
761 },
762 ],
763 nft_transfers: [],
764 expected_decimals: Some(
765 3,
766 ),
767 },
768 TokenTransferList {
769 token: Some(
770 TokenId {
771 shard_num: 0,
772 realm_num: 0,
773 token_num: 4,
774 },
775 ),
776 transfers: [
777 AccountAmount {
778 account_id: Some(
779 AccountId {
780 shard_num: 0,
781 realm_num: 0,
782 account: Some(
783 AccountNum(
784 5008,
785 ),
786 ),
787 },
788 ),
789 amount: 1,
790 is_approval: false,
791 hook_call: None,
792 },
793 AccountAmount {
794 account_id: Some(
795 AccountId {
796 shard_num: 0,
797 realm_num: 0,
798 account: Some(
799 AccountNum(
800 5006,
801 ),
802 ),
803 },
804 ),
805 amount: -1,
806 is_approval: true,
807 hook_call: None,
808 },
809 ],
810 nft_transfers: [],
811 expected_decimals: None,
812 },
813 TokenTransferList {
814 token: Some(
815 TokenId {
816 shard_num: 0,
817 realm_num: 0,
818 token_num: 3,
819 },
820 ),
821 transfers: [],
822 nft_transfers: [
823 NftTransfer {
824 sender_account_id: Some(
825 AccountId {
826 shard_num: 0,
827 realm_num: 0,
828 account: Some(
829 AccountNum(
830 5008,
831 ),
832 ),
833 },
834 ),
835 receiver_account_id: Some(
836 AccountId {
837 shard_num: 0,
838 realm_num: 0,
839 account: Some(
840 AccountNum(
841 5007,
842 ),
843 ),
844 },
845 ),
846 serial_number: 2,
847 is_approval: false,
848 sender_allowance_hook_call: None,
849 receiver_allowance_hook_call: None,
850 },
851 NftTransfer {
852 sender_account_id: Some(
853 AccountId {
854 shard_num: 0,
855 realm_num: 0,
856 account: Some(
857 AccountNum(
858 5008,
859 ),
860 ),
861 },
862 ),
863 receiver_account_id: Some(
864 AccountId {
865 shard_num: 0,
866 realm_num: 0,
867 account: Some(
868 AccountNum(
869 5007,
870 ),
871 ),
872 },
873 ),
874 serial_number: 1,
875 is_approval: true,
876 sender_allowance_hook_call: None,
877 receiver_allowance_hook_call: None,
878 },
879 NftTransfer {
880 sender_account_id: Some(
881 AccountId {
882 shard_num: 0,
883 realm_num: 0,
884 account: Some(
885 AccountNum(
886 5008,
887 ),
888 ),
889 },
890 ),
891 receiver_account_id: Some(
892 AccountId {
893 shard_num: 0,
894 realm_num: 0,
895 account: Some(
896 AccountNum(
897 5006,
898 ),
899 ),
900 },
901 ),
902 serial_number: 3,
903 is_approval: false,
904 sender_allowance_hook_call: None,
905 receiver_allowance_hook_call: None,
906 },
907 NftTransfer {
908 sender_account_id: Some(
909 AccountId {
910 shard_num: 0,
911 realm_num: 0,
912 account: Some(
913 AccountNum(
914 5007,
915 ),
916 ),
917 },
918 ),
919 receiver_account_id: Some(
920 AccountId {
921 shard_num: 0,
922 realm_num: 0,
923 account: Some(
924 AccountNum(
925 5006,
926 ),
927 ),
928 },
929 ),
930 serial_number: 4,
931 is_approval: false,
932 sender_allowance_hook_call: None,
933 receiver_allowance_hook_call: None,
934 },
935 ],
936 expected_decimals: None,
937 },
938 TokenTransferList {
939 token: Some(
940 TokenId {
941 shard_num: 0,
942 realm_num: 0,
943 token_num: 2,
944 },
945 ),
946 transfers: [],
947 nft_transfers: [
948 NftTransfer {
949 sender_account_id: Some(
950 AccountId {
951 shard_num: 0,
952 realm_num: 0,
953 account: Some(
954 AccountNum(
955 5007,
956 ),
957 ),
958 },
959 ),
960 receiver_account_id: Some(
961 AccountId {
962 shard_num: 0,
963 realm_num: 0,
964 account: Some(
965 AccountNum(
966 5006,
967 ),
968 ),
969 },
970 ),
971 serial_number: 4,
972 is_approval: false,
973 sender_allowance_hook_call: None,
974 receiver_allowance_hook_call: None,
975 },
976 ],
977 expected_decimals: None,
978 },
979 ],
980 },
981 )
982 "#]]
983 .assert_debug_eq(&tx)
984 }
985
986 #[test]
987 fn to_from_bytes() {
988 let tx = make_transaction();
989
990 let tx2 = AnyTransaction::from_bytes(&tx.to_bytes().unwrap()).unwrap();
991
992 let tx = transaction_body(tx);
993
994 let tx2 = transaction_body(tx2);
995
996 assert_eq!(tx, tx2);
997 }
998
999 #[test]
1000 fn get_decimals() {
1001 let mut tx = TransferTransaction::new();
1002 const TOKEN: TokenId = TokenId::new(0, 0, 5);
1003
1004 assert_eq!(tx.get_token_decimals().get(&TOKEN), None);
1005
1006 tx.token_transfer(TOKEN, AccountId::new(0, 0, 8), 100);
1007 assert_eq!(tx.get_token_decimals().get(&TOKEN), None);
1008
1009 tx.token_transfer_with_decimals(TOKEN, AccountId::new(0, 0, 7), -100, 5);
1010 assert_eq!(tx.get_token_decimals().get(&TOKEN), Some(&5));
1011 }
1012
1013 #[test]
1014 fn token_transfer_aggregation_same_token_and_account() {
1015 let mut tx = TransferTransaction::new();
1017 const TOKEN: TokenId = TokenId::new(0, 0, 5);
1018 const ACCOUNT: AccountId = AccountId::new(0, 0, 100);
1019
1020 tx.token_transfer(TOKEN, ACCOUNT, 100);
1022 tx.token_transfer(TOKEN, ACCOUNT, 200);
1023 tx.token_transfer(TOKEN, ACCOUNT, 300);
1024
1025 let transfers = tx.get_token_transfers();
1026 let account_transfers = transfers.get(&TOKEN).unwrap();
1027
1028 assert_eq!(account_transfers.len(), 1);
1030 assert_eq!(account_transfers.get(&ACCOUNT), Some(&600));
1031 }
1032
1033 #[test]
1034 fn token_transfer_aggregation_different_accounts() {
1035 let mut tx = TransferTransaction::new();
1037 const TOKEN: TokenId = TokenId::new(0, 0, 5);
1038 const ACCOUNT1: AccountId = AccountId::new(0, 0, 100);
1039 const ACCOUNT2: AccountId = AccountId::new(0, 0, 200);
1040
1041 tx.token_transfer(TOKEN, ACCOUNT1, 100);
1042 tx.token_transfer(TOKEN, ACCOUNT2, 200);
1043 tx.token_transfer(TOKEN, ACCOUNT1, 300);
1044
1045 let transfers = tx.get_token_transfers();
1046 let account_transfers = transfers.get(&TOKEN).unwrap();
1047
1048 assert_eq!(account_transfers.len(), 2);
1050 assert_eq!(account_transfers.get(&ACCOUNT1), Some(&400)); assert_eq!(account_transfers.get(&ACCOUNT2), Some(&200));
1052 }
1053
1054 #[test]
1055 fn token_transfer_aggregation_different_tokens() {
1056 let mut tx = TransferTransaction::new();
1058 const TOKEN1: TokenId = TokenId::new(0, 0, 5);
1059 const TOKEN2: TokenId = TokenId::new(0, 0, 6);
1060 const ACCOUNT: AccountId = AccountId::new(0, 0, 100);
1061
1062 tx.token_transfer(TOKEN1, ACCOUNT, 100);
1063 tx.token_transfer(TOKEN2, ACCOUNT, 200);
1064 tx.token_transfer(TOKEN1, ACCOUNT, 300);
1065
1066 let transfers = tx.get_token_transfers();
1067
1068 assert_eq!(transfers.len(), 2);
1070 assert_eq!(transfers.get(&TOKEN1).unwrap().get(&ACCOUNT), Some(&400)); assert_eq!(transfers.get(&TOKEN2).unwrap().get(&ACCOUNT), Some(&200));
1072 }
1073
1074 #[test]
1075 fn token_transfer_aggregation_approved_vs_non_approved() {
1076 let mut tx = TransferTransaction::new();
1078 const TOKEN: TokenId = TokenId::new(0, 0, 5);
1079 const ACCOUNT: AccountId = AccountId::new(0, 0, 100);
1080
1081 tx.token_transfer(TOKEN, ACCOUNT, 100);
1082 tx.approved_token_transfer(TOKEN, ACCOUNT, 200);
1083 tx.token_transfer(TOKEN, ACCOUNT, 300);
1084 tx.approved_token_transfer(TOKEN, ACCOUNT, 400);
1085
1086 let data = tx.data();
1088 let token_transfer = data.token_transfers.iter().find(|tt| tt.token_id == TOKEN).unwrap();
1089
1090 assert_eq!(token_transfer.transfers.len(), 2);
1092
1093 let non_approved = token_transfer
1094 .transfers
1095 .iter()
1096 .find(|t| !t.is_approval && t.account_id == ACCOUNT)
1097 .unwrap();
1098 assert_eq!(non_approved.amount, 400); let approved = token_transfer
1101 .transfers
1102 .iter()
1103 .find(|t| t.is_approval && t.account_id == ACCOUNT)
1104 .unwrap();
1105 assert_eq!(approved.amount, 600); }
1107
1108 #[test]
1109 fn token_transfer_with_decimals_aggregation() {
1110 let mut tx = TransferTransaction::new();
1112 const TOKEN: TokenId = TokenId::new(0, 0, 5);
1113 const ACCOUNT: AccountId = AccountId::new(0, 0, 100);
1114
1115 tx.token_transfer_with_decimals(TOKEN, ACCOUNT, 100, 3);
1116 tx.token_transfer_with_decimals(TOKEN, ACCOUNT, 200, 3);
1117 tx.token_transfer_with_decimals(TOKEN, ACCOUNT, 300, 3);
1118
1119 let transfers = tx.get_token_transfers();
1120 let account_transfers = transfers.get(&TOKEN).unwrap();
1121
1122 assert_eq!(account_transfers.len(), 1);
1124 assert_eq!(account_transfers.get(&ACCOUNT), Some(&600));
1125
1126 assert_eq!(tx.get_token_decimals().get(&TOKEN), Some(&3));
1128 }
1129
1130 #[test]
1131 fn token_transfer_negative_amounts_aggregate() {
1132 let mut tx = TransferTransaction::new();
1134 const TOKEN: TokenId = TokenId::new(0, 0, 5);
1135 const SENDER: AccountId = AccountId::new(0, 0, 100);
1136 const RECEIVER: AccountId = AccountId::new(0, 0, 200);
1137
1138 tx.token_transfer(TOKEN, SENDER, -100);
1139 tx.token_transfer(TOKEN, SENDER, -200);
1140 tx.token_transfer(TOKEN, RECEIVER, 150);
1141 tx.token_transfer(TOKEN, RECEIVER, 150);
1142
1143 let transfers = tx.get_token_transfers();
1144 let account_transfers = transfers.get(&TOKEN).unwrap();
1145
1146 assert_eq!(account_transfers.get(&SENDER), Some(&-300)); assert_eq!(account_transfers.get(&RECEIVER), Some(&300)); }
1149}