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::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        // 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                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        // Comes with asc order.
236        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        // 5. Mint another MANAGED coin to account, balance 10
242        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        // 6. Put the balance 10 MANAGED coin into the envelope
282        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        // 7. take back the balance 10 MANAGED coin
311        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        // 8. Put the balance = 10 MANAGED coin back to envelope
346        let _ = add_to_envelope(ctx, package.0, envelope.0, managed_coin_id).await;
347
348        // 9. Take from envelope and burn
349        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        // Values are the same as in the end of step 6
377        assert_eq!(managed_balance.total_balance, managed_old_total_balance);
378        assert_eq!(managed_balance.coin_object_count, managed_old_total_count);
379
380        // 10. Burn the balance=10000 MANAGED coin
381        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        // =========================== Test Get Coins Starts ===========================
410
411        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            // this is only SUI coins at the moment
429            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        // 11. Mint 40 MANAGED coins with balance 5
449        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"))?,  // balance = 5
460                    SuiJsonValue::new(json!("40"))?, // num = 40
461                    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        // No more even if ask for more
480        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        // 12. add one coin to envelope, now we only have 39 coins
599        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        // =========================== Test Get Coins Ends ===========================
628
629        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(/* with_unpublished_deps */ 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        // Doesn't need to be scaled by RGP since most of the cost is storage
647        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}