sui_cluster_test/test_case/
coin_index_test.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use 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        // 0. Get some coins first
40        ctx.get_sui_from_faucet(None).await;
41
42        // Record initial balances
43        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        // 1. Execute one transfer coin transaction (to another address)
50        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        // 2. Test Staking
100        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); // an object is staked
129        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        // 3. Publish a new token package MANAGED
140        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        // 4. Mint 1 MANAGED coin to account, balance 10000
154        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            // Important: update coin_type_str here because the leading 0s are truncated!
206            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); // minted one object
213        assert_eq!(
214            managed_total_balance,
215            10000, // mint amount
216        );
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        // Comes with asc order.
234        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        // 5. Mint another MANAGED coin to account, balance 10
240        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        // 6. Put the balance 10 MANAGED coin into the envelope
280        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        // 7. take back the balance 10 MANAGED coin
309        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        // 8. Put the balance = 10 MANAGED coin back to envelope
344        let _ = add_to_envelope(ctx, package.0, envelope.0, managed_coin_id).await;
345
346        // 9. Take from envelope and burn
347        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        // Values are the same as in the end of step 6
375        assert_eq!(managed_balance.total_balance, managed_old_total_balance);
376        assert_eq!(managed_balance.coin_object_count, managed_old_total_count);
377
378        // 10. Burn the balance=10000 MANAGED coin
379        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        // =========================== Test Get Coins Starts ===========================
408
409        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            // this is only SUI coins at the moment
427            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        // 11. Mint 40 MANAGED coins with balance 5
447        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"))?,  // balance = 5
458                    SuiJsonValue::new(json!("40"))?, // num = 40
459                    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        // No more even if ask for more
478        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        // 12. add one coin to envelope, now we only have 39 coins
597        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        // =========================== Test Get Coins Ends ===========================
626
627        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(/* with_unpublished_deps */ 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        // Doesn't need to be scaled by RGP since most of the cost is storage
645        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}