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::transaction_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 funds_in_address_balance: 0,
226 },
227 Balance {
228 coin_type: coin_type_str.clone(),
229 coin_object_count: 1,
230 total_balance: 10000,
231 locked_balance: HashMap::new(),
232 funds_in_address_balance: 0,
233 },
234 ];
235 expected_balances.sort_by(|l: &Balance, r| l.coin_type.cmp(&r.coin_type));
237 balances.sort_by(|l: &Balance, r| l.coin_type.cmp(&r.coin_type));
238
239 assert_eq!(balances, expected_balances,);
240
241 let txn = client
243 .transaction_builder()
244 .move_call(
245 account,
246 package.0,
247 "managed",
248 "mint",
249 vec![],
250 vec![
251 SuiJsonValue::from_object_id(cap.0),
252 SuiJsonValue::new(json!("10"))?,
253 SuiJsonValue::new(json!(account))?,
254 ],
255 None,
256 rgp * 2_000_000,
257 None,
258 )
259 .await
260 .unwrap();
261 let response = ctx.sign_and_execute(txn, "mint managed coin to self").await;
262 assert!(response.status_ok().unwrap());
263
264 let managed_balance = client
265 .coin_read_api()
266 .get_balance(account, Some(coin_type_str.clone()))
267 .await
268 .unwrap();
269 let managed_coins = client
270 .coin_read_api()
271 .get_coins(account, Some(coin_type_str.clone()), None, None)
272 .await
273 .unwrap()
274 .data;
275 assert_eq!(managed_balance.total_balance, 10000 + 10);
276 assert_eq!(managed_balance.coin_object_count, 1 + 1);
277 assert_eq!(managed_coins.len(), 1 + 1);
278 let managed_old_total_balance = managed_balance.total_balance;
279 let managed_old_total_count = managed_balance.coin_object_count;
280
281 let managed_coin_id = managed_coins
283 .iter()
284 .find(|c| c.balance == 10)
285 .unwrap()
286 .coin_object_id;
287 let managed_coin_id_10k = managed_coins
288 .iter()
289 .find(|c| c.balance == 10000)
290 .unwrap()
291 .coin_object_id;
292 let _ = add_to_envelope(ctx, package.0, envelope.0, managed_coin_id).await;
293
294 let managed_balance = client
295 .coin_read_api()
296 .get_balance(account, Some(coin_type_str.clone()))
297 .await
298 .unwrap();
299 assert_eq!(
300 managed_balance.total_balance,
301 managed_old_total_balance - 10
302 );
303 assert_eq!(
304 managed_balance.coin_object_count,
305 managed_old_total_count - 1
306 );
307 let managed_old_total_balance = managed_balance.total_balance;
308 let managed_old_total_count = managed_balance.coin_object_count;
309
310 let args = vec![SuiJsonValue::from_object_id(envelope.0)];
312 let txn = client
313 .transaction_builder()
314 .move_call(
315 account,
316 package.0,
317 "managed",
318 "take_from_envelope",
319 vec![],
320 args,
321 None,
322 rgp * 2_000_000,
323 None,
324 )
325 .await
326 .unwrap();
327 let response = ctx
328 .sign_and_execute(txn, "take back managed coin from envelope")
329 .await;
330 assert!(response.status_ok().unwrap());
331 let managed_balance = client
332 .coin_read_api()
333 .get_balance(account, Some(coin_type_str.clone()))
334 .await
335 .unwrap();
336 assert_eq!(
337 managed_balance.total_balance,
338 managed_old_total_balance + 10
339 );
340 assert_eq!(
341 managed_balance.coin_object_count,
342 managed_old_total_count + 1
343 );
344
345 let _ = add_to_envelope(ctx, package.0, envelope.0, managed_coin_id).await;
347
348 let txn = client
350 .transaction_builder()
351 .move_call(
352 account,
353 package.0,
354 "managed",
355 "take_from_envelope_and_burn",
356 vec![],
357 vec![
358 SuiJsonValue::from_object_id(cap.0),
359 SuiJsonValue::from_object_id(envelope.0),
360 ],
361 None,
362 rgp * 2_000_000,
363 None,
364 )
365 .await
366 .unwrap();
367 let response = ctx
368 .sign_and_execute(txn, "take back managed coin from envelope and burn")
369 .await;
370 assert!(response.status_ok().unwrap());
371 let managed_balance = client
372 .coin_read_api()
373 .get_balance(account, Some(coin_type_str.clone()))
374 .await
375 .unwrap();
376 assert_eq!(managed_balance.total_balance, managed_old_total_balance);
378 assert_eq!(managed_balance.coin_object_count, managed_old_total_count);
379
380 let txn = client
382 .transaction_builder()
383 .move_call(
384 account,
385 package.0,
386 "managed",
387 "burn",
388 vec![],
389 vec![
390 SuiJsonValue::from_object_id(cap.0),
391 SuiJsonValue::from_object_id(managed_coin_id_10k),
392 ],
393 None,
394 rgp * 2_000_000,
395 None,
396 )
397 .await
398 .unwrap();
399 let response = ctx.sign_and_execute(txn, "burn coin").await;
400 assert!(response.status_ok().unwrap());
401 let managed_balance = client
402 .coin_read_api()
403 .get_balance(account, Some(coin_type_str.clone()))
404 .await
405 .unwrap();
406 assert_eq!(managed_balance.total_balance, 0);
407 assert_eq!(managed_balance.coin_object_count, 0);
408
409 let sui_coins = client
412 .coin_read_api()
413 .get_coins(account, Some(sui_type_str.into()), None, None)
414 .await
415 .unwrap()
416 .data;
417
418 assert_eq!(
419 sui_coins,
420 client
421 .coin_read_api()
422 .get_coins(account, None, None, None)
423 .await
424 .unwrap()
425 .data,
426 );
427 assert_eq!(
428 sui_coins,
430 client
431 .coin_read_api()
432 .get_all_coins(account, None, None)
433 .await
434 .unwrap()
435 .data,
436 );
437
438 let sui_balance = client
439 .coin_read_api()
440 .get_balance(account, None)
441 .await
442 .unwrap();
443 assert_eq!(
444 sui_balance.total_balance,
445 sui_coins.iter().map(|c| c.balance as u128).sum::<u128>()
446 );
447
448 let txn = client
450 .transaction_builder()
451 .move_call(
452 account,
453 package.0,
454 "managed",
455 "mint_multi",
456 vec![],
457 vec![
458 SuiJsonValue::from_object_id(cap.0),
459 SuiJsonValue::new(json!("5"))?, SuiJsonValue::new(json!("40"))?, SuiJsonValue::new(json!(account))?,
462 ],
463 None,
464 rgp * 2_000_000,
465 None,
466 )
467 .await
468 .unwrap();
469 let response = ctx.sign_and_execute(txn, "multi mint").await;
470 assert!(response.status_ok().unwrap());
471
472 let sui_coins = client
473 .coin_read_api()
474 .get_coins(account, Some(sui_type_str.into()), None, None)
475 .await
476 .unwrap()
477 .data;
478
479 assert_eq!(
481 sui_coins,
482 client
483 .coin_read_api()
484 .get_coins(account, None, None, Some(sui_coins.len() + 1))
485 .await
486 .unwrap()
487 .data,
488 );
489
490 let managed_coins = client
491 .coin_read_api()
492 .get_coins(account, Some(coin_type_str.clone()), None, None)
493 .await
494 .unwrap()
495 .data;
496 let first_managed_coin = managed_coins.first().unwrap().coin_object_id;
497 let last_managed_coin = managed_coins.last().unwrap().coin_object_id;
498
499 assert_eq!(managed_coins.len(), 40);
500 assert!(managed_coins.iter().all(|c| c.balance == 5));
501
502 let mut total_coins = 0;
503 let mut cursor = None;
504 loop {
505 let page = client
506 .coin_read_api()
507 .get_all_coins(account, cursor, None)
508 .await
509 .unwrap();
510 total_coins += page.data.len();
511 cursor = page.next_cursor;
512 if !page.has_next_page {
513 break;
514 }
515 }
516
517 assert_eq!(sui_coins.len() + managed_coins.len(), total_coins,);
518
519 let sui_coins_with_managed_coin_1 = client
520 .coin_read_api()
521 .get_all_coins(account, None, Some(sui_coins.len() + 1))
522 .await
523 .unwrap();
524 assert_eq!(
525 sui_coins_with_managed_coin_1.data.len(),
526 sui_coins.len() + 1
527 );
528 assert!(sui_coins_with_managed_coin_1.has_next_page);
529 let cursor = sui_coins_with_managed_coin_1.next_cursor;
530
531 let managed_coins_2_11 = client
532 .coin_read_api()
533 .get_all_coins(account, cursor.clone(), Some(10))
534 .await
535 .unwrap();
536 assert_eq!(
537 managed_coins_2_11,
538 client
539 .coin_read_api()
540 .get_coins(account, Some(coin_type_str.clone()), cursor, Some(10))
541 .await
542 .unwrap(),
543 );
544
545 assert_eq!(managed_coins_2_11.data.len(), 10);
546 assert_ne!(
547 managed_coins_2_11.data.first().unwrap().coin_object_id,
548 first_managed_coin
549 );
550 assert!(managed_coins_2_11.has_next_page);
551 let cursor = managed_coins_2_11.next_cursor;
552
553 let managed_coins_12_40 = client
554 .coin_read_api()
555 .get_all_coins(account, cursor.clone(), None)
556 .await
557 .unwrap();
558 assert_eq!(
559 managed_coins_12_40,
560 client
561 .coin_read_api()
562 .get_coins(account, Some(coin_type_str.clone()), cursor.clone(), None)
563 .await
564 .unwrap(),
565 );
566 assert_eq!(managed_coins_12_40.data.len(), 29);
567 assert_eq!(
568 managed_coins_12_40.data.last().unwrap().coin_object_id,
569 last_managed_coin
570 );
571 assert!(!managed_coins_12_40.has_next_page);
572
573 let managed_coins_12_40 = client
574 .coin_read_api()
575 .get_all_coins(account, cursor.clone(), Some(30))
576 .await
577 .unwrap();
578 assert_eq!(
579 managed_coins_12_40,
580 client
581 .coin_read_api()
582 .get_coins(
583 account,
584 Some(coin_type_str.clone()),
585 cursor.clone(),
586 Some(30)
587 )
588 .await
589 .unwrap(),
590 );
591 assert_eq!(managed_coins_12_40.data.len(), 29);
592 assert_eq!(
593 managed_coins_12_40.data.last().unwrap().coin_object_id,
594 last_managed_coin
595 );
596 assert!(!managed_coins_12_40.has_next_page);
597
598 let removed_coin_id = managed_coins.get(20).unwrap().coin_object_id;
600 let _ = add_to_envelope(ctx, package.0, envelope.0, removed_coin_id).await;
601 let managed_coins_12_39 = client
602 .coin_read_api()
603 .get_all_coins(account, cursor.clone(), Some(40))
604 .await
605 .unwrap();
606 assert_eq!(
607 managed_coins_12_39,
608 client
609 .coin_read_api()
610 .get_coins(account, Some(coin_type_str.clone()), cursor, Some(40))
611 .await
612 .unwrap(),
613 );
614 assert_eq!(managed_coins_12_39.data.len(), 28);
615 assert_eq!(
616 managed_coins_12_39.data.last().unwrap().coin_object_id,
617 last_managed_coin
618 );
619 assert!(
620 !managed_coins_12_39
621 .data
622 .iter()
623 .any(|coin| coin.coin_object_id == removed_coin_id)
624 );
625 assert!(!managed_coins_12_39.has_next_page);
626
627 Ok(())
630 }
631}
632
633async fn publish_managed_coin_package(
634 ctx: &mut TestContext,
635) -> Result<(ObjectRef, ObjectRef, ObjectRef), anyhow::Error> {
636 let compiled_package = compile_managed_coin_package().await;
637 let all_module_bytes =
638 compiled_package.get_package_base64(false);
639 let dependencies = compiled_package.get_dependency_storage_package_ids();
640
641 let params = rpc_params![
642 ctx.get_wallet_address(),
643 all_module_bytes,
644 dependencies,
645 None::<ObjectID>,
646 500_000_000.to_string()
648 ];
649
650 let data = ctx
651 .build_transaction_remotely("unsafe_publish", params)
652 .await?;
653 let response = ctx.sign_and_execute(data, "publish ft package").await;
654 let changes = response.object_changes.unwrap();
655 info!("changes: {:?}", changes);
656 let pkg = changes
657 .iter()
658 .find(|change| matches!(change, ObjectChange::Published { .. }))
659 .unwrap()
660 .object_ref();
661 let treasury_cap = changes
662 .iter()
663 .find(|change| {
664 matches!(change, ObjectChange::Created {
665 owner: Owner::AddressOwner(_),
666 object_type: StructTag {
667 name,
668 ..
669 },
670 ..
671 } if name.as_str() == "TreasuryCap")
672 })
673 .unwrap()
674 .object_ref();
675 let envelope = changes
676 .iter()
677 .find(|change| {
678 matches!(change, ObjectChange::Created {
679 owner: Owner::Shared {..},
680 object_type: StructTag {
681 name,
682 ..
683 },
684 ..
685 } if name.as_str() == "PublicRedEnvelope")
686 })
687 .unwrap()
688 .object_ref();
689 Ok((pkg, treasury_cap, envelope))
690}
691
692async fn add_to_envelope(
693 ctx: &mut TestContext,
694 pkg_id: ObjectID,
695 envelope: ObjectID,
696 coin: ObjectID,
697) -> SuiTransactionBlockResponse {
698 let account = ctx.get_wallet_address();
699 let client = ctx.clone_fullnode_client();
700 let rgp = ctx.get_reference_gas_price().await;
701 let txn = client
702 .transaction_builder()
703 .move_call(
704 account,
705 pkg_id,
706 "managed",
707 "add_to_envelope",
708 vec![],
709 vec![
710 SuiJsonValue::from_object_id(envelope),
711 SuiJsonValue::from_object_id(coin),
712 ],
713 None,
714 rgp * 2_000_000,
715 None,
716 )
717 .await
718 .unwrap();
719 let response = ctx
720 .sign_and_execute(txn, "add managed coin to envelope")
721 .await;
722 assert!(response.status_ok().unwrap());
723 response
724}