1use std::collections::HashMap;
4
5use hiero_sdk_proto::services;
6use hiero_sdk_proto::services::token_service_client::TokenServiceClient;
7use tonic::transport::Channel;
8
9use super::{
10 NftId,
11 TokenId,
12 TokenNftTransfer,
13};
14use crate::hooks::FungibleHookCall;
15use crate::ledger_id::RefLedgerId;
16use crate::protobuf::{
17 FromProtobuf,
18 ToProtobuf,
19};
20use crate::transaction::{
21 AnyTransactionData,
22 ChunkInfo,
23 ToSchedulableTransactionDataProtobuf,
24 ToTransactionDataProtobuf,
25 TransactionData,
26 TransactionExecute,
27};
28use crate::transfer_transaction::{
29 TokenTransfer,
30 Transfer,
31};
32use crate::{
33 AccountId,
34 BoxGrpcFuture,
35 Error,
36 Transaction,
37 ValidateChecksums,
38};
39
40pub type TokenAirdropTransaction = Transaction<TokenAirdropTransactionData>;
88
89#[derive(Debug, Clone, Default)]
90pub struct TokenAirdropTransactionData {
91 token_transfers: Vec<TokenTransfer>,
93}
94
95impl TokenAirdropTransaction {
96 pub fn token_transfer(
98 &mut self,
99 token_id: TokenId,
100 account_id: AccountId,
101 value: i64,
102 ) -> &mut Self {
103 self._token_transfer(token_id, account_id, value, false, None)
104 }
105
106 pub fn get_token_transfers(&self) -> HashMap<TokenId, HashMap<AccountId, i64>> {
108 use std::collections::hash_map::Entry;
109
110 self.data().token_transfers.iter().fold(
112 HashMap::with_capacity(self.data().token_transfers.len()),
113 |mut map, transfer| {
114 let iter = transfer.transfers.iter().map(|it| (it.account_id, it.amount));
115 match map.entry(transfer.token_id) {
116 Entry::Occupied(mut it) => it.get_mut().extend(iter),
117 Entry::Vacant(it) => {
118 it.insert(iter.collect());
119 }
120 }
121
122 map
123 },
124 )
125 }
126
127 pub fn nft_transfer(
129 &mut self,
130 nft_id: NftId,
131 sender: AccountId,
132 receiver: AccountId,
133 ) -> &mut Self {
134 self._nft_transfer(nft_id, sender, receiver, false);
135 self
136 }
137
138 pub fn get_nft_transfers(&self) -> HashMap<TokenId, Vec<TokenNftTransfer>> {
140 self.data().token_transfers.iter().map(|t| (t.token_id, t.nft_transfers.clone())).collect()
141 }
142
143 pub fn token_transfer_with_decimals(
145 &mut self,
146 token_id: TokenId,
147 account_id: AccountId,
148 amount: i64,
149 decimals: u32,
150 ) -> &mut Self {
151 self._token_transfer_with_decimals(token_id, account_id, amount, false, Some(decimals));
152 self
153 }
154
155 pub fn get_token_ids_with_decimals(&self) -> HashMap<TokenId, Option<u32>> {
157 self.data().token_transfers.iter().map(|t| (t.token_id, t.expected_decimals)).collect()
158 }
159
160 pub fn approved_token_transfer(
162 &mut self,
163 token_id: TokenId,
164 account_id: AccountId,
165 amount: i64,
166 ) -> &mut Self {
167 self._token_transfer(token_id, account_id, amount, true, None);
168 self
169 }
170
171 pub fn approved_nft_transfer(
173 &mut self,
174 nft_id: NftId,
175 sender: AccountId,
176 receiver: AccountId,
177 ) -> &mut Self {
178 self._nft_transfer(nft_id, sender, receiver, true);
179 self
180 }
181
182 pub fn approved_token_transfer_with_decimals(
184 &mut self,
185 token_id: TokenId,
186 account_id: AccountId,
187 amount: i64,
188 decimals: u32,
189 ) -> &mut Self {
190 self._token_transfer_with_decimals(token_id, account_id, amount, true, Some(decimals));
191 self
192 }
193
194 fn _token_transfer(
195 &mut self,
196 token_id: TokenId,
197 account_id: AccountId,
198 amount: i64,
199 is_approved: bool,
200 hook_call: Option<FungibleHookCall>,
201 ) -> &mut Self {
202 let transfer = Transfer { account_id, amount, is_approval: is_approved, hook_call };
203 let data = self.data_mut();
204
205 if let Some(tt) = data.token_transfers.iter_mut().find(|tt| tt.token_id == token_id) {
206 if let Some(tt) = tt
207 .transfers
208 .iter_mut()
209 .find(|t| t.account_id == account_id && t.is_approval == is_approved)
210 {
211 tt.amount += amount;
212 } else {
213 tt.transfers.push(transfer);
214 }
215 } else {
216 data.token_transfers.push(TokenTransfer {
217 token_id,
218 expected_decimals: None,
219 nft_transfers: Vec::new(),
220 transfers: vec![transfer],
221 });
222 }
223 self
224 }
225
226 fn _token_transfer_with_decimals(
227 &mut self,
228 token_id: TokenId,
229 account_id: AccountId,
230 amount: i64,
231 approved: bool,
232 expected_decimals: Option<u32>,
233 ) -> &mut Self {
234 let transfer = Transfer { account_id, amount, is_approval: approved, hook_call: None };
235 let data = self.data_mut();
236
237 if let Some(tt) = data.token_transfers.iter_mut().find(|tt| tt.token_id == token_id) {
238 if tt.expected_decimals.is_some() && tt.expected_decimals != expected_decimals {
239 panic!("expected decimals mismatch");
240 }
241
242 tt.expected_decimals = expected_decimals;
243
244 if let Some(tt) = tt
245 .transfers
246 .iter_mut()
247 .find(|t| t.account_id == account_id && t.is_approval == approved)
248 {
249 tt.amount += amount;
250 } else {
251 tt.transfers.push(transfer);
252 }
253 } else {
254 data.token_transfers.push(TokenTransfer {
255 token_id,
256 expected_decimals,
257 nft_transfers: Vec::new(),
258 transfers: vec![transfer],
259 });
260 }
261 self
262 }
263
264 fn _nft_transfer(
265 &mut self,
266 nft_id: NftId,
267 sender: AccountId,
268 receiver: AccountId,
269 is_approved: bool,
270 ) -> &mut Self {
271 let NftId { token_id, serial } = nft_id;
272 let transfer = TokenNftTransfer {
273 token_id,
274 serial,
275 sender,
276 receiver,
277 is_approved,
278 sender_hook_call: None,
279 receiver_hook_call: None,
280 };
281
282 let data = self.data_mut();
283
284 if let Some(tt) = data.token_transfers.iter_mut().find(|tt| tt.token_id == token_id) {
285 tt.nft_transfers.push(transfer);
286 } else {
287 data.token_transfers.push(TokenTransfer {
288 token_id,
289 expected_decimals: None,
290 transfers: Vec::new(),
291 nft_transfers: vec![transfer],
292 });
293 }
294
295 self
296 }
297}
298
299impl TransactionData for TokenAirdropTransactionData {}
300
301impl TransactionExecute for TokenAirdropTransactionData {
302 fn execute(
303 &self,
304 channel: Channel,
305 request: services::Transaction,
306 ) -> BoxGrpcFuture<'_, services::TransactionResponse> {
307 Box::pin(async { TokenServiceClient::new(channel).airdrop_tokens(request).await })
308 }
309}
310
311impl ValidateChecksums for TokenAirdropTransactionData {
312 fn validate_checksums(&self, ledger_id: &RefLedgerId) -> Result<(), Error> {
313 for token_transfer in &self.token_transfers {
314 token_transfer.token_id.validate_checksums(ledger_id)?;
315 for transfer in &token_transfer.transfers {
316 transfer.account_id.validate_checksums(ledger_id)?;
317 }
318 for nft_transfer in &token_transfer.nft_transfers {
319 nft_transfer.sender.validate_checksums(ledger_id)?;
320 nft_transfer.receiver.validate_checksums(ledger_id)?;
321 }
322 }
323 Ok(())
324 }
325}
326
327impl ToTransactionDataProtobuf for TokenAirdropTransactionData {
328 fn to_transaction_data_protobuf(
329 &self,
330 chunk_info: &ChunkInfo,
331 ) -> services::transaction_body::Data {
332 let _ = chunk_info.assert_single_transaction();
333
334 services::transaction_body::Data::TokenAirdrop(self.to_protobuf())
335 }
336}
337
338impl ToSchedulableTransactionDataProtobuf for TokenAirdropTransactionData {
339 fn to_schedulable_transaction_data_protobuf(
340 &self,
341 ) -> services::schedulable_transaction_body::Data {
342 services::schedulable_transaction_body::Data::TokenAirdrop(self.to_protobuf())
343 }
344}
345
346impl From<TokenAirdropTransactionData> for AnyTransactionData {
347 fn from(transaction: TokenAirdropTransactionData) -> Self {
348 Self::TokenAirdrop(transaction)
349 }
350}
351
352impl ToProtobuf for TokenAirdropTransactionData {
353 type Protobuf = services::TokenAirdropTransactionBody;
354
355 fn to_protobuf(&self) -> Self::Protobuf {
356 let mut token_transfers = self.token_transfers.clone();
357
358 token_transfers.sort_by(|a, b| {
360 a.token_id
361 .shard
362 .cmp(&b.token_id.shard)
363 .then(a.token_id.realm.cmp(&b.token_id.realm))
364 .then(a.token_id.num.cmp(&b.token_id.num))
365 });
366
367 for tt in &mut token_transfers {
369 tt.transfers.sort_by(|a, b| {
370 a.account_id
371 .shard
372 .cmp(&b.account_id.shard)
373 .then_with(|| a.account_id.realm.cmp(&b.account_id.realm))
374 .then_with(|| a.account_id.num.cmp(&b.account_id.num))
375 .then_with(|| a.is_approval.cmp(&b.is_approval))
376 });
377
378 tt.nft_transfers.sort_by(|a, b| a.serial.cmp(&b.serial));
379 }
380
381 services::TokenAirdropTransactionBody {
382 token_transfers: token_transfers
383 .into_iter()
384 .map(|tt| services::TokenTransferList {
385 token: Some(tt.token_id.to_protobuf()),
386 transfers: tt.transfers.into_iter().map(|t| t.to_protobuf()).collect(),
387 nft_transfers: tt
388 .nft_transfers
389 .into_iter()
390 .map(|nt| nt.to_protobuf())
391 .collect(),
392 expected_decimals: tt.expected_decimals.map(|d| d as u32),
393 })
394 .collect(),
395 }
396 }
397}
398
399impl FromProtobuf<services::TokenAirdropTransactionBody> for TokenAirdropTransactionData {
400 fn from_protobuf(pb: services::TokenAirdropTransactionBody) -> crate::Result<Self>
401 where
402 Self: Sized,
403 {
404 Ok(Self {
405 token_transfers: pb
406 .token_transfers
407 .into_iter()
408 .map(|t| TokenTransfer::from_protobuf(t))
409 .collect::<crate::Result<_>>()?,
410 })
411 }
412}
413
414#[cfg(test)]
415mod tests {
416 use std::str::FromStr;
417
418 use expect_test::expect_file;
419 use hiero_sdk_proto::services::{
420 self,
421 AccountAmount,
422 NftTransfer,
423 TokenTransferList,
424 };
425
426 use crate::protobuf::{
427 FromProtobuf,
428 ToProtobuf,
429 };
430 use crate::token::TokenAirdropTransactionData;
431 use crate::transaction::test_helpers::{
432 check_body,
433 transaction_body,
434 unused_private_key,
435 TEST_ACCOUNT_ID,
436 TEST_TOKEN_ID,
437 };
438 use crate::{
439 AccountId,
440 AnyTransaction,
441 TokenAirdropTransaction,
442 TokenId,
443 };
444
445 fn make_transaction() -> TokenAirdropTransaction {
446 let mut tx = TokenAirdropTransaction::new_for_tests();
447
448 tx.token_transfer(TokenId::new(0, 0, 5005), AccountId::new(0, 0, 5006), 400)
449 .token_transfer_with_decimals(
450 TokenId::new(0, 0, 5),
451 AccountId::new(0, 0, 5005),
452 -800,
453 3,
454 )
455 .token_transfer_with_decimals(
456 TokenId::new(0, 0, 5),
457 AccountId::new(0, 0, 5007),
458 -400,
459 3,
460 )
461 .token_transfer(TokenId::new(0, 0, 4), AccountId::new(0, 0, 5008), 1)
462 .token_transfer(TokenId::new(0, 0, 4), AccountId::new(0, 0, 5006), -1)
463 .nft_transfer(
464 TokenId::new(0, 0, 3).nft(2),
465 AccountId::new(0, 0, 5008),
466 AccountId::new(0, 0, 5007),
467 )
468 .nft_transfer(
469 TokenId::new(0, 0, 3).nft(1),
470 AccountId::new(0, 0, 5008),
471 AccountId::new(0, 0, 5007),
472 )
473 .nft_transfer(
474 TokenId::new(0, 0, 3).nft(3),
475 AccountId::new(0, 0, 5008),
476 AccountId::new(0, 0, 5006),
477 )
478 .nft_transfer(
479 TokenId::new(0, 0, 3).nft(4),
480 AccountId::new(0, 0, 5007),
481 AccountId::new(0, 0, 5006),
482 )
483 .nft_transfer(
484 TokenId::new(0, 0, 2).nft(4),
485 AccountId::new(0, 0, 5007),
486 AccountId::new(0, 0, 5006),
487 )
488 .approved_token_transfer(TokenId::new(0, 0, 4), AccountId::new(0, 0, 5006), 123)
489 .approved_nft_transfer(
490 TokenId::new(0, 0, 4).nft(4),
491 AccountId::new(0, 0, 5005),
492 AccountId::new(0, 0, 5006),
493 )
494 .freeze()
495 .unwrap()
496 .sign(unused_private_key());
497 tx
498 }
499
500 #[test]
501 fn serialize() {
502 let tx = make_transaction();
503
504 let tx = transaction_body(tx);
505
506 let tx = check_body(tx);
507
508 expect_file!["./snapshots/token_airdrop_transaction/serialize.txt"].assert_debug_eq(&tx);
509 }
510
511 #[test]
512 fn to_from_bytes() {
513 let tx = make_transaction();
514
515 let tx2 = AnyTransaction::from_bytes(&tx.to_bytes().unwrap()).unwrap();
516
517 let tx = transaction_body(tx);
518 let tx2 = transaction_body(tx2);
519
520 println!("tx: {:?}", tx);
521 println!("tx2: {:?}", tx2);
522
523 assert_eq!(tx, tx2)
524 }
525
526 #[test]
527 fn from_proto_body() {
528 let tx = services::TokenAirdropTransactionBody {
529 token_transfers: vec![TokenTransferList {
530 token: Some(TEST_TOKEN_ID.to_protobuf()),
531 transfers: vec![
532 AccountAmount {
533 account_id: Some(AccountId::from_str("0.0.5008").unwrap().to_protobuf()),
534 amount: 200,
535 is_approval: false,
536 hook_call: None,
537 },
538 AccountAmount {
539 account_id: Some(AccountId::from_str("0.0.5009").unwrap().to_protobuf()),
540 amount: -100,
541 is_approval: false,
542 hook_call: None,
543 },
544 AccountAmount {
545 account_id: Some(AccountId::from_str("0.0.5010").unwrap().to_protobuf()),
546 amount: 40,
547 is_approval: false,
548 hook_call: None,
549 },
550 AccountAmount {
551 account_id: Some(AccountId::from_str("0.0.5011").unwrap().to_protobuf()),
552 amount: 20,
553 is_approval: false,
554 hook_call: None,
555 },
556 ],
557 nft_transfers: vec![NftTransfer {
558 sender_account_id: Some(AccountId::from_str("0.0.5010").unwrap().to_protobuf()),
559 receiver_account_id: Some(
560 AccountId::from_str("0.0.5011").unwrap().to_protobuf(),
561 ),
562 serial_number: 1,
563 is_approval: true,
564 sender_allowance_hook_call: None,
565 receiver_allowance_hook_call: None,
566 }],
567 expected_decimals: Some(3),
568 }],
569 };
570
571 let data = TokenAirdropTransactionData::from_protobuf(tx).unwrap();
572
573 let ft_transfers =
574 data.token_transfers.iter().flat_map(|t| &t.transfers).collect::<Vec<_>>();
575 let nft_transfers =
576 data.token_transfers.iter().flat_map(|t| &t.nft_transfers).collect::<Vec<_>>();
577
578 assert_eq!(ft_transfers.len(), 4);
579 assert_eq!(nft_transfers.len(), 1);
580 }
581
582 #[test]
583 fn get_set_token_transfers() {
584 let token_id = TokenId::new(0, 0, 123);
585 let account_id = AccountId::new(0, 0, 456);
586 let value = 1000;
587 let mut tx = TokenAirdropTransaction::new();
588 tx.token_transfer(token_id, account_id, value);
589
590 let token_transfers = tx.get_token_transfers();
591
592 assert!(token_transfers.contains_key(&token_id));
593 assert_eq!(token_transfers.len(), 1);
594 assert_eq!(value, *token_transfers.get(&token_id).unwrap().get(&account_id).unwrap());
595 }
596
597 #[test]
598 #[should_panic]
599 fn get_set_token_transfers_frozen_panic() {
600 make_transaction().token_transfer(TEST_TOKEN_ID, TEST_ACCOUNT_ID, 142);
601 }
602
603 #[test]
604 fn get_set_nft_transfer() {
605 let (nft_id, sender, receiver) =
606 (TEST_TOKEN_ID.nft(1), TEST_ACCOUNT_ID, AccountId::new(0, 0, 5011));
607 let mut tx = TokenAirdropTransaction::new();
608 tx.nft_transfer(nft_id, sender, receiver);
609 let nft_transfers = tx.get_nft_transfers();
610
611 assert!(nft_transfers.contains_key(&nft_id.token_id));
612 assert_eq!(1, nft_transfers.get(&nft_id.token_id).unwrap().len());
613 assert_eq!(sender, nft_transfers.get(&nft_id.token_id).unwrap()[0].sender);
614 assert_eq!(receiver, nft_transfers.get(&nft_id.token_id).unwrap()[0].receiver);
615 }
616
617 #[test]
618 #[should_panic]
619 fn get_set_nft_transfer_frozen_panic() {
620 make_transaction().nft_transfer(
621 TEST_TOKEN_ID.nft(1),
622 TEST_ACCOUNT_ID,
623 AccountId::new(0, 0, 156),
624 );
625 }
626
627 #[test]
628 fn get_set_approved_nft_transfer() {
629 let (nft_id, sender, receiver) =
630 (TEST_TOKEN_ID.nft(1), TEST_ACCOUNT_ID, AccountId::new(0, 0, 123));
631 let mut tx = TokenAirdropTransaction::new();
632 tx.approved_nft_transfer(nft_id, sender, receiver);
633 let nft_transfers = tx.get_nft_transfers();
634
635 assert!(nft_transfers.contains_key(&nft_id.token_id));
636 assert_eq!(nft_transfers.get(&nft_id.token_id).unwrap().len(), 1);
637 assert_eq!(sender, nft_transfers.get(&nft_id.token_id).unwrap()[0].sender);
638 assert_eq!(receiver, nft_transfers.get(&nft_id.token_id).unwrap()[0].receiver);
639 }
640
641 #[test]
642 fn get_set_approved_token_transfer() {
643 let (token_id, account_id, value) =
644 (TokenId::new(0, 0, 1420), AccountId::new(0, 0, 415), 1000);
645 let mut tx = TokenAirdropTransaction::new();
646 tx.approved_token_transfer(token_id, account_id, value);
647
648 let token_transfers = tx.get_token_transfers();
649
650 assert!(token_transfers.contains_key(&token_id));
651 assert_eq!(token_transfers.len(), 1);
652 assert_eq!(value, *token_transfers.get(&token_id).unwrap().get(&account_id).unwrap());
653 }
654
655 #[test]
656 fn get_set_token_id_decimals() {
657 let (nft_id, sender, receiver) =
658 (TEST_TOKEN_ID.nft(1), TEST_ACCOUNT_ID, AccountId::new(0, 0, 123));
659 let mut tx = TokenAirdropTransaction::new();
660 tx.approved_nft_transfer(nft_id, sender, receiver);
661 let nft_transfers = tx.get_nft_transfers();
662
663 assert!(nft_transfers.contains_key(&nft_id.token_id));
664 assert_eq!(nft_transfers.get(&nft_id.token_id).unwrap().len(), 1);
665 assert_eq!(sender, nft_transfers.get(&nft_id.token_id).unwrap()[0].sender);
666 assert_eq!(receiver, nft_transfers.get(&nft_id.token_id).unwrap()[0].receiver);
667 }
668}