iota_cluster_test/test_case/
coin_index_test.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use 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        // 0. Get some coins first
46        ctx.get_iota_from_faucet(None).await;
47
48        // Record initial balances
49        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        // 1. Execute one transfer coin transaction (to another address)
56        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        // 2. Test Staking
106        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); // an object is staked
136        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        // 3. Publish a new token package MANAGED
147        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        // 4. Mint 1 MANAGED coin to account, balance 10000
161        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            // Important: update coin_type_str here because the leading 0s are truncated!
213            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); // minted one object
220        assert_eq!(
221            managed_total_balance,
222            10000, // mint amount
223        );
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        // Comes with asc order.
239        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        // 5. Mint another MANAGED coin to account, balance 10
245        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        // 6. Put the balance 10 MANAGED coin into the envelope
285        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        // 7. take back the balance 10 MANAGED coin
314        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        // 8. Put the balance = 10 MANAGED coin back to envelope
349        let _ = add_to_envelope(ctx, package.0, envelope.0, managed_coin_id).await;
350
351        // 9. Take from envelope and burn
352        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        // Values are the same as in the end of step 6
380        assert_eq!(managed_balance.total_balance, managed_old_total_balance);
381        assert_eq!(managed_balance.coin_object_count, managed_old_total_count);
382
383        // 10. Burn the balance=10000 MANAGED coin
384        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        // =========================== Test Get Coins Starts ===========================
413
414        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            // this is only IOTA coins at the moment
432            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        // 11. Mint 40 MANAGED coins with balance 5
452        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"))?,  // balance = 5
463                    IotaJsonValue::new(json!("40"))?, // num = 40
464                    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        // No more even if ask for more
483        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        // 12. add one coin to envelope, now we only have 39 coins
595        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        // =========================== Test Get Coins Ends ===========================
624
625        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(/* with_unpublished_deps */ 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        // Doesn't need to be scaled by RGP since most of the cost is storage
643        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}