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