1use async_trait::async_trait;
6use futures::StreamExt;
7use iota_core::test_utils::compile_managed_coin_package;
8use iota_json::IotaJsonValue;
9use iota_json_rpc_types::{
10 Balance, IotaTransactionBlockResponse, IotaTransactionBlockResponseOptions, ObjectChange,
11};
12use iota_sdk::PagedFn;
13use iota_test_transaction_builder::make_staking_transaction;
14use iota_types::{
15 base_types::{ObjectID, ObjectRef},
16 gas_coin::GAS,
17 iota_system_state::iota_system_state_summary::IotaSystemStateSummary,
18 object::Owner,
19 quorum_driver_types::ExecuteTransactionRequestType,
20};
21use jsonrpsee::rpc_params;
22use move_core_types::language_storage::StructTag;
23use serde_json::json;
24use tracing::info;
25
26use crate::{TestCaseImpl, TestContext};
27
28pub struct CoinIndexTest;
29
30#[async_trait]
31impl TestCaseImpl for CoinIndexTest {
32 fn name(&self) -> &'static str {
33 "CoinIndex"
34 }
35
36 fn description(&self) -> &'static str {
37 "Test coin index"
38 }
39
40 async fn run(&self, ctx: &mut TestContext) -> Result<(), anyhow::Error> {
41 let account = ctx.get_wallet_address();
42 let client = ctx.clone_fullnode_client();
43 let rgp = ctx.get_reference_gas_price().await;
44
45 ctx.get_iota_from_faucet(None).await;
47
48 let Balance {
50 coin_object_count: mut old_coin_object_count,
51 total_balance: mut old_total_balance,
52 ..
53 } = client.coin_read_api().get_balance(account, None).await?;
54
55 let txn = ctx.make_transactions(1).await.swap_remove(0);
57 let response = client
58 .quorum_driver_api()
59 .execute_transaction_block(
60 txn,
61 IotaTransactionBlockResponseOptions::new()
62 .with_effects()
63 .with_balance_changes(),
64 Some(ExecuteTransactionRequestType::WaitForLocalExecution),
65 )
66 .await?;
67
68 let balance_change = response.balance_changes.unwrap();
69 let owner_balance = balance_change
70 .iter()
71 .find(|b| b.owner == Owner::AddressOwner(account))
72 .unwrap();
73 let recipient_balance = balance_change
74 .iter()
75 .find(|b| b.owner != Owner::AddressOwner(account))
76 .unwrap();
77 let Balance {
78 coin_object_count,
79 total_balance,
80 coin_type,
81 ..
82 } = client.coin_read_api().get_balance(account, None).await?;
83 assert_eq!(coin_type, GAS::type_().to_string());
84
85 assert_eq!(coin_object_count, old_coin_object_count);
86 assert_eq!(
87 total_balance,
88 (old_total_balance as i128 + owner_balance.amount) as u128
89 );
90 old_coin_object_count = coin_object_count;
91 old_total_balance = total_balance;
92
93 let Balance {
94 coin_object_count,
95 total_balance,
96 ..
97 } = client
98 .coin_read_api()
99 .get_balance(recipient_balance.owner.get_owner_address().unwrap(), None)
100 .await?;
101 assert_eq!(coin_object_count, 1);
102 assert!(recipient_balance.amount > 0);
103 assert_eq!(total_balance, recipient_balance.amount as u128);
104
105 let validator_addr = match ctx.get_latest_iota_system_state().await {
107 IotaSystemStateSummary::V1(v1) => v1.active_validators,
108 IotaSystemStateSummary::V2(v2) => v2.active_validators,
109 _ => panic!("unsupported IotaSystemStateSummary"),
110 }
111 .first()
112 .unwrap()
113 .iota_address;
114 let txn = make_staking_transaction(ctx.get_wallet(), validator_addr).await;
115
116 let response = client
117 .quorum_driver_api()
118 .execute_transaction_block(
119 txn,
120 IotaTransactionBlockResponseOptions::new()
121 .with_effects()
122 .with_balance_changes(),
123 Some(ExecuteTransactionRequestType::WaitForLocalExecution),
124 )
125 .await?;
126
127 let balance_change = &response.balance_changes.unwrap()[0];
128 assert_eq!(balance_change.owner, Owner::AddressOwner(account));
129
130 let Balance {
131 coin_object_count,
132 total_balance,
133 ..
134 } = client.coin_read_api().get_balance(account, None).await?;
135 assert_eq!(coin_object_count, old_coin_object_count - 1); assert_eq!(
137 total_balance,
138 (old_total_balance as i128 + balance_change.amount) as u128,
139 "total_balance: {}, old_total_balance: {}, iota_balance_change.amount: {}",
140 total_balance,
141 old_total_balance,
142 balance_change.amount
143 );
144 old_coin_object_count = coin_object_count;
145
146 let (package, cap, envelope) = publish_managed_coin_package(ctx).await?;
148 let Balance { total_balance, .. } =
149 client.coin_read_api().get_balance(account, None).await?;
150 old_total_balance = total_balance;
151
152 info!(
153 "token package published, package: {:?}, cap: {:?}",
154 package, cap
155 );
156 let iota_type_str = "0x2::iota::IOTA";
157 let coin_type_str = format!("{}::managed::MANAGED", package.0);
158 info!("coin type: {}", coin_type_str);
159
160 let args = vec![
162 IotaJsonValue::from_object_id(cap.0),
163 IotaJsonValue::new(json!("10000"))?,
164 IotaJsonValue::new(json!(account))?,
165 ];
166 let txn = client
167 .transaction_builder()
168 .move_call(
169 account,
170 package.0,
171 "managed",
172 "mint",
173 vec![],
174 args,
175 None,
176 rgp * 2_000_000,
177 None,
178 )
179 .await
180 .unwrap();
181 let response = ctx.sign_and_execute(txn, "mint managed coin to self").await;
182
183 let balance_changes = &response.balance_changes.unwrap();
184 let iota_balance_change = balance_changes
185 .iter()
186 .find(|b| b.coin_type.to_string().contains("IOTA"))
187 .unwrap();
188 let managed_balance_change = balance_changes
189 .iter()
190 .find(|b| b.coin_type.to_string().contains("MANAGED"))
191 .unwrap();
192
193 assert_eq!(iota_balance_change.owner, Owner::AddressOwner(account));
194 assert_eq!(managed_balance_change.owner, Owner::AddressOwner(account));
195
196 let Balance { total_balance, .. } =
197 client.coin_read_api().get_balance(account, None).await?;
198 assert_eq!(coin_object_count, old_coin_object_count);
199 assert_eq!(
200 total_balance,
201 (old_total_balance as i128 + iota_balance_change.amount) as u128,
202 "total_balance: {}, old_total_balance: {}, iota_balance_change.amount: {}",
203 total_balance,
204 old_total_balance,
205 iota_balance_change.amount
206 );
207 old_coin_object_count = coin_object_count;
208
209 let Balance {
210 coin_object_count: managed_coin_object_count,
211 total_balance: managed_total_balance,
212 coin_type: coin_type_str,
214 ..
215 } = client
216 .coin_read_api()
217 .get_balance(account, Some(coin_type_str.clone()))
218 .await?;
219 assert_eq!(managed_coin_object_count, 1); assert_eq!(
221 managed_total_balance,
222 10000, );
224
225 let mut balances = client.coin_read_api().get_all_balances(account).await?;
226 let mut expected_balances = vec![
227 Balance {
228 coin_type: iota_type_str.into(),
229 coin_object_count: old_coin_object_count,
230 total_balance,
231 },
232 Balance {
233 coin_type: coin_type_str.clone(),
234 coin_object_count: 1,
235 total_balance: 10000,
236 },
237 ];
238 expected_balances.sort_by(|l: &Balance, r| l.coin_type.cmp(&r.coin_type));
240 balances.sort_by(|l: &Balance, r| l.coin_type.cmp(&r.coin_type));
241
242 assert_eq!(balances, expected_balances,);
243
244 let txn = client
246 .transaction_builder()
247 .move_call(
248 account,
249 package.0,
250 "managed",
251 "mint",
252 vec![],
253 vec![
254 IotaJsonValue::from_object_id(cap.0),
255 IotaJsonValue::new(json!("10"))?,
256 IotaJsonValue::new(json!(account))?,
257 ],
258 None,
259 rgp * 2_000_000,
260 None,
261 )
262 .await
263 .unwrap();
264 let response = ctx.sign_and_execute(txn, "mint managed coin to self").await;
265 assert!(response.status_ok().unwrap());
266
267 let managed_balance = client
268 .coin_read_api()
269 .get_balance(account, Some(coin_type_str.clone()))
270 .await
271 .unwrap();
272 let managed_coins = client
273 .coin_read_api()
274 .get_coins(account, Some(coin_type_str.clone()), None, None)
275 .await
276 .unwrap()
277 .data;
278 assert_eq!(managed_balance.total_balance, 10000 + 10);
279 assert_eq!(managed_balance.coin_object_count, 1 + 1);
280 assert_eq!(managed_coins.len(), 1 + 1);
281 let managed_old_total_balance = managed_balance.total_balance;
282 let managed_old_total_count = managed_balance.coin_object_count;
283
284 let managed_coin_id = managed_coins
286 .iter()
287 .find(|c| c.balance == 10)
288 .unwrap()
289 .coin_object_id;
290 let managed_coin_id_10k = managed_coins
291 .iter()
292 .find(|c| c.balance == 10000)
293 .unwrap()
294 .coin_object_id;
295 let _ = add_to_envelope(ctx, package.0, envelope.0, managed_coin_id).await;
296
297 let managed_balance = client
298 .coin_read_api()
299 .get_balance(account, Some(coin_type_str.clone()))
300 .await
301 .unwrap();
302 assert_eq!(
303 managed_balance.total_balance,
304 managed_old_total_balance - 10
305 );
306 assert_eq!(
307 managed_balance.coin_object_count,
308 managed_old_total_count - 1
309 );
310 let managed_old_total_balance = managed_balance.total_balance;
311 let managed_old_total_count = managed_balance.coin_object_count;
312
313 let args = vec![IotaJsonValue::from_object_id(envelope.0)];
315 let txn = client
316 .transaction_builder()
317 .move_call(
318 account,
319 package.0,
320 "managed",
321 "take_from_envelope",
322 vec![],
323 args,
324 None,
325 rgp * 2_000_000,
326 None,
327 )
328 .await
329 .unwrap();
330 let response = ctx
331 .sign_and_execute(txn, "take back managed coin from envelope")
332 .await;
333 assert!(response.status_ok().unwrap());
334 let managed_balance = client
335 .coin_read_api()
336 .get_balance(account, Some(coin_type_str.clone()))
337 .await
338 .unwrap();
339 assert_eq!(
340 managed_balance.total_balance,
341 managed_old_total_balance + 10
342 );
343 assert_eq!(
344 managed_balance.coin_object_count,
345 managed_old_total_count + 1
346 );
347
348 let _ = add_to_envelope(ctx, package.0, envelope.0, managed_coin_id).await;
350
351 let txn = client
353 .transaction_builder()
354 .move_call(
355 account,
356 package.0,
357 "managed",
358 "take_from_envelope_and_burn",
359 vec![],
360 vec![
361 IotaJsonValue::from_object_id(cap.0),
362 IotaJsonValue::from_object_id(envelope.0),
363 ],
364 None,
365 rgp * 2_000_000,
366 None,
367 )
368 .await
369 .unwrap();
370 let response = ctx
371 .sign_and_execute(txn, "take back managed coin from envelope and burn")
372 .await;
373 assert!(response.status_ok().unwrap());
374 let managed_balance = client
375 .coin_read_api()
376 .get_balance(account, Some(coin_type_str.clone()))
377 .await
378 .unwrap();
379 assert_eq!(managed_balance.total_balance, managed_old_total_balance);
381 assert_eq!(managed_balance.coin_object_count, managed_old_total_count);
382
383 let txn = client
385 .transaction_builder()
386 .move_call(
387 account,
388 package.0,
389 "managed",
390 "burn",
391 vec![],
392 vec![
393 IotaJsonValue::from_object_id(cap.0),
394 IotaJsonValue::from_object_id(managed_coin_id_10k),
395 ],
396 None,
397 rgp * 2_000_000,
398 None,
399 )
400 .await
401 .unwrap();
402 let response = ctx.sign_and_execute(txn, "burn coin").await;
403 assert!(response.status_ok().unwrap());
404 let managed_balance = client
405 .coin_read_api()
406 .get_balance(account, Some(coin_type_str.clone()))
407 .await
408 .unwrap();
409 assert_eq!(managed_balance.total_balance, 0);
410 assert_eq!(managed_balance.coin_object_count, 0);
411
412 let iota_coins = client
415 .coin_read_api()
416 .get_coins(account, Some(iota_type_str.into()), None, None)
417 .await
418 .unwrap()
419 .data;
420
421 assert_eq!(
422 iota_coins,
423 client
424 .coin_read_api()
425 .get_coins(account, None, None, None)
426 .await
427 .unwrap()
428 .data,
429 );
430 assert_eq!(
431 iota_coins,
433 client
434 .coin_read_api()
435 .get_all_coins(account, None, None)
436 .await
437 .unwrap()
438 .data,
439 );
440
441 let iota_balance = client
442 .coin_read_api()
443 .get_balance(account, None)
444 .await
445 .unwrap();
446 assert_eq!(
447 iota_balance.total_balance,
448 iota_coins.iter().map(|c| c.balance as u128).sum::<u128>()
449 );
450
451 let txn = client
453 .transaction_builder()
454 .move_call(
455 account,
456 package.0,
457 "managed",
458 "mint_multi",
459 vec![],
460 vec![
461 IotaJsonValue::from_object_id(cap.0),
462 IotaJsonValue::new(json!("5"))?, IotaJsonValue::new(json!("40"))?, IotaJsonValue::new(json!(account))?,
465 ],
466 None,
467 rgp * 2_000_000,
468 None,
469 )
470 .await
471 .unwrap();
472 let response = ctx.sign_and_execute(txn, "multi mint").await;
473 assert!(response.status_ok().unwrap());
474
475 let iota_coins = client
476 .coin_read_api()
477 .get_coins(account, Some(iota_type_str.into()), None, None)
478 .await
479 .unwrap()
480 .data;
481
482 assert_eq!(
484 iota_coins,
485 client
486 .coin_read_api()
487 .get_coins(account, None, None, Some(iota_coins.len() + 1))
488 .await
489 .unwrap()
490 .data,
491 );
492
493 let managed_coins = client
494 .coin_read_api()
495 .get_coins(account, Some(coin_type_str.clone()), None, None)
496 .await
497 .unwrap()
498 .data;
499 let first_managed_coin = managed_coins.first().unwrap().coin_object_id;
500 let last_managed_coin = managed_coins.last().unwrap().coin_object_id;
501
502 assert_eq!(managed_coins.len(), 40);
503 assert!(managed_coins.iter().all(|c| c.balance == 5));
504
505 let total_coins = PagedFn::stream(async |cursor| {
506 client
507 .coin_read_api()
508 .get_all_coins(account, cursor, None)
509 .await
510 })
511 .count()
512 .await;
513
514 assert_eq!(iota_coins.len() + managed_coins.len(), total_coins);
515
516 let iota_coins_with_managed_coin_1 = client
517 .coin_read_api()
518 .get_all_coins(account, None, Some(iota_coins.len() + 1))
519 .await
520 .unwrap();
521 assert_eq!(
522 iota_coins_with_managed_coin_1.data.len(),
523 iota_coins.len() + 1
524 );
525 assert_eq!(
526 iota_coins_with_managed_coin_1.next_cursor,
527 Some(first_managed_coin)
528 );
529 assert!(iota_coins_with_managed_coin_1.has_next_page);
530 let cursor = iota_coins_with_managed_coin_1.next_cursor;
531
532 let managed_coins_2_11 = client
533 .coin_read_api()
534 .get_all_coins(account, cursor, Some(10))
535 .await
536 .unwrap();
537 assert_eq!(
538 managed_coins_2_11,
539 client
540 .coin_read_api()
541 .get_coins(account, Some(coin_type_str.clone()), cursor, Some(10))
542 .await
543 .unwrap(),
544 );
545
546 assert_eq!(managed_coins_2_11.data.len(), 10);
547 assert_ne!(
548 managed_coins_2_11.data.first().unwrap().coin_object_id,
549 first_managed_coin
550 );
551 assert!(managed_coins_2_11.has_next_page);
552 let cursor = managed_coins_2_11.next_cursor;
553
554 let managed_coins_12_40 = client
555 .coin_read_api()
556 .get_all_coins(account, cursor, None)
557 .await
558 .unwrap();
559 assert_eq!(
560 managed_coins_12_40,
561 client
562 .coin_read_api()
563 .get_coins(account, Some(coin_type_str.clone()), cursor, None)
564 .await
565 .unwrap(),
566 );
567 assert_eq!(managed_coins_12_40.data.len(), 29);
568 assert_eq!(
569 managed_coins_12_40.data.last().unwrap().coin_object_id,
570 last_managed_coin
571 );
572 assert!(!managed_coins_12_40.has_next_page);
573
574 let managed_coins_12_40 = client
575 .coin_read_api()
576 .get_all_coins(account, cursor, Some(30))
577 .await
578 .unwrap();
579 assert_eq!(
580 managed_coins_12_40,
581 client
582 .coin_read_api()
583 .get_coins(account, Some(coin_type_str.clone()), cursor, Some(30))
584 .await
585 .unwrap(),
586 );
587 assert_eq!(managed_coins_12_40.data.len(), 29);
588 assert_eq!(
589 managed_coins_12_40.data.last().unwrap().coin_object_id,
590 last_managed_coin
591 );
592 assert!(!managed_coins_12_40.has_next_page);
593
594 let removed_coin_id = managed_coins.get(20).unwrap().coin_object_id;
596 let _ = add_to_envelope(ctx, package.0, envelope.0, removed_coin_id).await;
597 let managed_coins_12_39 = client
598 .coin_read_api()
599 .get_all_coins(account, cursor, Some(40))
600 .await
601 .unwrap();
602 assert_eq!(
603 managed_coins_12_39,
604 client
605 .coin_read_api()
606 .get_coins(account, Some(coin_type_str.clone()), cursor, Some(40))
607 .await
608 .unwrap(),
609 );
610 assert_eq!(managed_coins_12_39.data.len(), 28);
611 assert_eq!(
612 managed_coins_12_39.data.last().unwrap().coin_object_id,
613 last_managed_coin
614 );
615 assert!(
616 !managed_coins_12_39
617 .data
618 .iter()
619 .any(|coin| coin.coin_object_id == removed_coin_id)
620 );
621 assert!(!managed_coins_12_39.has_next_page);
622
623 Ok(())
626 }
627}
628
629async fn publish_managed_coin_package(
630 ctx: &mut TestContext,
631) -> Result<(ObjectRef, ObjectRef, ObjectRef), anyhow::Error> {
632 let compiled_package = compile_managed_coin_package();
633 let all_module_bytes =
634 compiled_package.get_package_base64(false);
635 let dependencies = compiled_package.get_dependency_storage_package_ids();
636
637 let params = rpc_params![
638 ctx.get_wallet_address(),
639 all_module_bytes,
640 dependencies,
641 None::<ObjectID>,
642 500_000_000.to_string()
644 ];
645
646 let data = ctx
647 .build_transaction_remotely("unsafe_publish", params)
648 .await?;
649 let response = ctx.sign_and_execute(data, "publish ft package").await;
650 let changes = response.object_changes.unwrap();
651 info!("changes: {:?}", changes);
652 let pkg = changes
653 .iter()
654 .find(|change| matches!(change, ObjectChange::Published { .. }))
655 .unwrap()
656 .object_ref();
657 let treasury_cap = changes
658 .iter()
659 .find(|change| {
660 matches!(change, ObjectChange::Created {
661 owner: Owner::AddressOwner(_),
662 object_type: StructTag {
663 name,
664 ..
665 },
666 ..
667 } if name.as_str() == "TreasuryCap")
668 })
669 .unwrap()
670 .object_ref();
671 let envelope = changes
672 .iter()
673 .find(|change| {
674 matches!(change, ObjectChange::Created {
675 owner: Owner::Shared {..},
676 object_type: StructTag {
677 name,
678 ..
679 },
680 ..
681 } if name.as_str() == "PublicRedEnvelope")
682 })
683 .unwrap()
684 .object_ref();
685 Ok((pkg, treasury_cap, envelope))
686}
687
688async fn add_to_envelope(
689 ctx: &mut TestContext,
690 pkg_id: ObjectID,
691 envelope: ObjectID,
692 coin: ObjectID,
693) -> IotaTransactionBlockResponse {
694 let account = ctx.get_wallet_address();
695 let client = ctx.clone_fullnode_client();
696 let rgp = ctx.get_reference_gas_price().await;
697 let txn = client
698 .transaction_builder()
699 .move_call(
700 account,
701 pkg_id,
702 "managed",
703 "add_to_envelope",
704 vec![],
705 vec![
706 IotaJsonValue::from_object_id(envelope),
707 IotaJsonValue::from_object_id(coin),
708 ],
709 None,
710 rgp * 2_000_000,
711 None,
712 )
713 .await
714 .unwrap();
715 let response = ctx
716 .sign_and_execute(txn, "add managed coin to envelope")
717 .await;
718 assert!(response.status_ok().unwrap());
719 response
720}