From ad7295ebd1ae55af15863a2c9166ff8edf47191b Mon Sep 17 00:00:00 2001 From: Sang Kim Date: Wed, 11 Dec 2024 14:41:31 +0900 Subject: [PATCH 1/8] [Rosetta] Process a case where single GasCoin is transferred --- crates/sui-rosetta/src/operations.rs | 85 +++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 3 deletions(-) diff --git a/crates/sui-rosetta/src/operations.rs b/crates/sui-rosetta/src/operations.rs index 8ea99ad1d2275..1add3b06c2f93 100644 --- a/crates/sui-rosetta/src/operations.rs +++ b/crates/sui-rosetta/src/operations.rs @@ -558,6 +558,74 @@ impl Operations { }; balance_change.chain(gas) } + + fn process_single_gascoin_transfer( + coin_change_operations: &mut impl Iterator, + tx: SuiTransactionBlockKind, + sender: SuiAddress, + gas_used: i128, + ) -> Vec { + let mut is_single_gascoin_transfer = false; + match tx { + SuiTransactionBlockKind::ProgrammableTransaction(pt) => { + let SuiProgrammableTransactionBlock { + inputs: _, + commands, + } = &pt; + for command in commands { + match command { + SuiCommand::TransferObjects(objs, _) => { + if objs.len() == 1 { + match objs[0] { + SuiArgument::GasCoin => { + is_single_gascoin_transfer = true; + } + _ => {} + } + } + } + _ => {} + } + } + } + _ => {} + } + if !is_single_gascoin_transfer { + return vec![]; + }; + let mut operations = vec![]; + coin_change_operations.into_iter().for_each(|operation| { + match operation.type_ { + OperationType::Gas => { + // change gas account back to sender as the sender is the one + // who paid for the txn (this is the format Rosetta wants to process) + operations.push(Operation::gas(sender, gas_used)) + } + OperationType::SuiBalanceChange => { + operation.account.map(|account| { + let mut amount = match operation.amount { + Some(amount) => amount.value, + None => 0, + }; + if account.address == sender { + // sender's balance needs to be adjusted for gas + amount -= gas_used; + } else { + // recipient's balance needs to be adjusted for gas + amount += gas_used; + } + operations.push(Operation::pay_sui( + operation.status, + account.address, + amount, + )); + }); + } + _ => {} + } + }); + operations + } } impl Operations { @@ -592,7 +660,7 @@ impl Operations { - gas_summary.computation_cost as i128; let status = Some(effect.into_status().into()); - let ops = Operations::try_from_data(tx.data, status)?; + let ops = Operations::try_from_data(tx.data.clone(), status)?; let ops = ops.into_iter(); // We will need to subtract the operation amounts from the actual balance @@ -662,17 +730,28 @@ impl Operations { } // Extract coin change operations from balance changes - let coin_change_operations = Self::process_balance_change( + let mut coin_change_operations = Self::process_balance_change( gas_owner, gas_used, balance_changes, status, - accounted_balances, + accounted_balances.clone(), ); + let mut single_gascoin_transfer = vec![]; + if gas_owner != sender && accounted_balances.is_empty() { + single_gascoin_transfer = Self::process_single_gascoin_transfer( + &mut coin_change_operations, + tx.data.transaction().clone(), + sender, + gas_used, + ); + } + let ops: Operations = ops .into_iter() .chain(coin_change_operations) + .chain(single_gascoin_transfer) .chain(staking_balance) .collect(); From 8120d1aa038297266214f5e3a99d4ab8591a0262 Mon Sep 17 00:00:00 2001 From: Sang Kim Date: Wed, 11 Dec 2024 21:08:11 +0900 Subject: [PATCH 2/8] [Rosetta] Add e2e test for transferring single GasCoin --- crates/sui-rosetta/tests/end_to_end_tests.rs | 133 ++++++++++++++++++- 1 file changed, 129 insertions(+), 4 deletions(-) diff --git a/crates/sui-rosetta/tests/end_to_end_tests.rs b/crates/sui-rosetta/tests/end_to_end_tests.rs index 6150372626237..2a4cbf824fe4d 100644 --- a/crates/sui-rosetta/tests/end_to_end_tests.rs +++ b/crates/sui-rosetta/tests/end_to_end_tests.rs @@ -1,23 +1,34 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 +use anyhow::anyhow; +use rand::rngs::OsRng; +use rand::seq::IteratorRandom; +use rosetta_client::start_rosetta_test_server; use serde_json::json; +use shared_crypto::intent::Intent; use std::num::NonZeroUsize; use std::time::Duration; - -use rosetta_client::start_rosetta_test_server; -use sui_json_rpc_types::SuiTransactionBlockResponseOptions; +use sui_json_rpc_types::{ + SuiObjectDataOptions, SuiObjectResponseQuery, SuiTransactionBlockResponseOptions, +}; use sui_keys::keystore::AccountKeystore; use sui_rosetta::operations::Operations; -use sui_rosetta::types::Currencies; use sui_rosetta::types::{ AccountBalanceRequest, AccountBalanceResponse, AccountIdentifier, Currency, NetworkIdentifier, SubAccount, SubAccountType, SuiEnv, }; +use sui_rosetta::types::{Currencies, OperationType}; use sui_rosetta::CoinMetadataCache; use sui_sdk::rpc_types::{SuiExecutionStatus, SuiTransactionBlockEffectsAPI}; +use sui_sdk::SuiClient; use sui_swarm_config::genesis_config::{DEFAULT_GAS_AMOUNT, DEFAULT_NUMBER_OF_OBJECT_PER_ACCOUNT}; +use sui_types::base_types::{ObjectID, ObjectRef, SuiAddress}; +use sui_types::programmable_transaction_builder::ProgrammableTransactionBuilder; use sui_types::quorum_driver_types::ExecuteTransactionRequestType; +use sui_types::transaction::{ + Argument, InputObjectKind, Transaction, TransactionData, TEST_ONLY_GAS_UNIT_FOR_TRANSFER, +}; use sui_types::utils::to_sender_signed_transaction; use test_cluster::TestClusterBuilder; @@ -501,3 +512,117 @@ async fn test_pay_sui_multiple_times() { ); } } + +async fn get_random_sui( + client: &SuiClient, + sender: SuiAddress, + except: Vec, +) -> ObjectRef { + let coins = client + .read_api() + .get_owned_objects( + sender, + Some(SuiObjectResponseQuery::new_with_options( + SuiObjectDataOptions::new() + .with_type() + .with_owner() + .with_previous_transaction(), + )), + /* cursor */ None, + /* limit */ None, + ) + .await + .unwrap() + .data; + + let coin_resp = coins + .iter() + .filter(|object| { + let obj = object.object().unwrap(); + obj.is_gas_coin() && !except.contains(&obj.object_id) + }) + .choose(&mut OsRng) + .unwrap(); + + let coin = coin_resp.object().unwrap(); + (coin.object_id, coin.version, coin.digest) +} +#[tokio::test] +async fn test_transfer_single_gas_coin() { + let test_cluster = TestClusterBuilder::new().build().await; + let sender = test_cluster.get_address_0(); + let recipient = test_cluster.get_address_1(); + let client = test_cluster.wallet.get_client().await.unwrap(); + let keystore = &test_cluster.wallet.config.keystore; + + let pt = { + let mut builder = ProgrammableTransactionBuilder::new(); + builder.transfer_arg(recipient, Argument::GasCoin); + builder.finish() + }; + + let input_objects = pt + .input_objects() + .unwrap_or_default() + .iter() + .flat_map(|obj| { + if let InputObjectKind::ImmOrOwnedMoveObject((id, ..)) = obj { + Some(*id) + } else { + None + } + }) + .collect::>(); + let gas = vec![get_random_sui(&client, sender, input_objects).await]; + let gas_price = client + .governance_api() + .get_reference_gas_price() + .await + .unwrap(); + + let data = TransactionData::new_programmable( + sender, + gas, + pt, + TEST_ONLY_GAS_UNIT_FOR_TRANSFER * gas_price, + gas_price, + ); + + let signature = keystore + .sign_secure(&sender, &data, Intent::sui_transaction()) + .unwrap(); + + let response = client + .quorum_driver_api() + .execute_transaction_block( + Transaction::from_data(data.clone(), vec![signature]), + SuiTransactionBlockResponseOptions::new() + .with_effects() + .with_object_changes() + .with_balance_changes() + .with_input(), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await + .map_err(|e| anyhow!("TX execution failed for {data:#?}, error : {e}")) + .unwrap(); + + let coin_cache = CoinMetadataCache::new(client, NonZeroUsize::new(2).unwrap()); + let operations = Operations::try_from_response(response, &coin_cache) + .await + .unwrap(); + println!("operations: {operations:#?}"); + + let mut balance = 0; + operations + .into_iter() + .for_each(|op| { + if op.type_ == OperationType::Gas { + assert_eq!(op.account.unwrap().address, sender); + } + if op.type_ == OperationType::PaySui { + balance += op.amount.unwrap().value; + } + }); + assert_eq!(balance, 0); +} From 0b6a82681525598291ef4dfc58a2e705635d4d09 Mon Sep 17 00:00:00 2001 From: Sang Kim Date: Mon, 16 Dec 2024 15:08:37 +0900 Subject: [PATCH 3/8] [Rosetta] Support the case when a sponsor pays for single GasCoin trasnfers --- crates/sui-rosetta/src/operations.rs | 129 ++++++++++++++------------- 1 file changed, 68 insertions(+), 61 deletions(-) diff --git a/crates/sui-rosetta/src/operations.rs b/crates/sui-rosetta/src/operations.rs index 1add3b06c2f93..31a71775d043f 100644 --- a/crates/sui-rosetta/src/operations.rs +++ b/crates/sui-rosetta/src/operations.rs @@ -559,71 +559,80 @@ impl Operations { balance_change.chain(gas) } - fn process_single_gascoin_transfer( - coin_change_operations: &mut impl Iterator, - tx: SuiTransactionBlockKind, - sender: SuiAddress, - gas_used: i128, - ) -> Vec { - let mut is_single_gascoin_transfer = false; + fn is_single_gascoin_transfer(tx: SuiTransactionBlockKind) -> bool { match tx { SuiTransactionBlockKind::ProgrammableTransaction(pt) => { let SuiProgrammableTransactionBlock { inputs: _, commands, } = &pt; - for command in commands { - match command { + return commands + .into_iter() + .find(|command| match command { SuiCommand::TransferObjects(objs, _) => { - if objs.len() == 1 { - match objs[0] { - SuiArgument::GasCoin => { - is_single_gascoin_transfer = true; - } - _ => {} - } - } + objs.len() > 0 && objs[0] == SuiArgument::GasCoin } - _ => {} - } - } + _ => false, + }) + .is_some(); } _ => {} } - if !is_single_gascoin_transfer { - return vec![]; - }; + false + } + + fn process_single_gascoin_transfer( + coin_change_operations: &mut impl Iterator, + tx: SuiTransactionBlockKind, + prev_gas_owner: SuiAddress, + new_gas_owner: SuiAddress, + gas_used: i128, + ) -> Vec { let mut operations = vec![]; - coin_change_operations.into_iter().for_each(|operation| { - match operation.type_ { - OperationType::Gas => { - // change gas account back to sender as the sender is the one - // who paid for the txn (this is the format Rosetta wants to process) - operations.push(Operation::gas(sender, gas_used)) - } - OperationType::SuiBalanceChange => { - operation.account.map(|account| { - let mut amount = match operation.amount { - Some(amount) => amount.value, - None => 0, - }; - if account.address == sender { - // sender's balance needs to be adjusted for gas - amount -= gas_used; - } else { - // recipient's balance needs to be adjusted for gas - amount += gas_used; - } - operations.push(Operation::pay_sui( - operation.status, - account.address, - amount, - )); - }); + if Self::is_single_gascoin_transfer(tx) { + coin_change_operations.into_iter().for_each(|operation| { + match operation.type_ { + OperationType::Gas => { + // change gas account back to the previous owner as it is the one + // who paid for the txn (this is the format Rosetta wants to process) + operations.push(Operation::gas(prev_gas_owner, gas_used)) + } + OperationType::SuiBalanceChange => { + operation.account.map(|account| { + let mut amount = match operation.amount { + Some(amount) => amount, + None => return, + }; + let mut is_convert_to_pay_sui = false; + if account.address == prev_gas_owner { + // previous owner's balance needs to be adjusted for gas + amount.value -= gas_used; + is_convert_to_pay_sui = true; + } else if account.address == new_gas_owner { + // new owner's balance needs to be adjusted for gas + amount.value += gas_used; + is_convert_to_pay_sui = true; + } + if is_convert_to_pay_sui { + operations.push(Operation::pay_sui( + operation.status, + account.address, + amount.value, + )); + } else { + operations.push(Operation::balance_change( + operation.status, + account.address, + amount.value, + amount.currency, + )); + } + }); + } + _ => {} } - _ => {} - } - }); + }); + } operations } } @@ -738,15 +747,13 @@ impl Operations { accounted_balances.clone(), ); - let mut single_gascoin_transfer = vec![]; - if gas_owner != sender && accounted_balances.is_empty() { - single_gascoin_transfer = Self::process_single_gascoin_transfer( - &mut coin_change_operations, - tx.data.transaction().clone(), - sender, - gas_used, - ); - } + let single_gascoin_transfer = Self::process_single_gascoin_transfer( + &mut coin_change_operations, + tx.data.transaction().clone(), + tx.data.gas_data().owner, + gas_owner, + gas_used, + ); let ops: Operations = ops .into_iter() From ce31fb1a2855cd0f057113aeb747f1df337df613 Mon Sep 17 00:00:00 2001 From: Sang Kim Date: Tue, 17 Dec 2024 11:37:15 +0900 Subject: [PATCH 4/8] [Rosetta] Handle the case when GasCoin is not the only argument passed in to objectTransfers --- crates/sui-rosetta/src/operations.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/sui-rosetta/src/operations.rs b/crates/sui-rosetta/src/operations.rs index 31a71775d043f..aff0ca7cd078d 100644 --- a/crates/sui-rosetta/src/operations.rs +++ b/crates/sui-rosetta/src/operations.rs @@ -559,7 +559,7 @@ impl Operations { balance_change.chain(gas) } - fn is_single_gascoin_transfer(tx: SuiTransactionBlockKind) -> bool { + fn is_gascoin_transfer(tx: SuiTransactionBlockKind) -> bool { match tx { SuiTransactionBlockKind::ProgrammableTransaction(pt) => { let SuiProgrammableTransactionBlock { @@ -570,7 +570,7 @@ impl Operations { .into_iter() .find(|command| match command { SuiCommand::TransferObjects(objs, _) => { - objs.len() > 0 && objs[0] == SuiArgument::GasCoin + objs.iter().any(|&obj| obj == SuiArgument::GasCoin) } _ => false, }) @@ -581,7 +581,7 @@ impl Operations { false } - fn process_single_gascoin_transfer( + fn process_gascoin_transfer( coin_change_operations: &mut impl Iterator, tx: SuiTransactionBlockKind, prev_gas_owner: SuiAddress, @@ -589,7 +589,7 @@ impl Operations { gas_used: i128, ) -> Vec { let mut operations = vec![]; - if Self::is_single_gascoin_transfer(tx) { + if Self::is_gascoin_transfer(tx) { coin_change_operations.into_iter().for_each(|operation| { match operation.type_ { OperationType::Gas => { @@ -747,7 +747,7 @@ impl Operations { accounted_balances.clone(), ); - let single_gascoin_transfer = Self::process_single_gascoin_transfer( + let gascoin_transfer_operations = Self::process_gascoin_transfer( &mut coin_change_operations, tx.data.transaction().clone(), tx.data.gas_data().owner, @@ -758,7 +758,7 @@ impl Operations { let ops: Operations = ops .into_iter() .chain(coin_change_operations) - .chain(single_gascoin_transfer) + .chain(gascoin_transfer_operations) .chain(staking_balance) .collect(); From b5fe666427e3b4253a556e8b47db19d2fba79965 Mon Sep 17 00:00:00 2001 From: Sang Kim Date: Wed, 18 Dec 2024 17:53:34 +0900 Subject: [PATCH 5/8] [Rosetta] Add more comments and a sanity check to make sure all the operations are processed --- crates/sui-rosetta/src/operations.rs | 44 ++++++++++++++++++---------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/crates/sui-rosetta/src/operations.rs b/crates/sui-rosetta/src/operations.rs index aff0ca7cd078d..7d9f8ef858786 100644 --- a/crates/sui-rosetta/src/operations.rs +++ b/crates/sui-rosetta/src/operations.rs @@ -559,6 +559,7 @@ impl Operations { balance_change.chain(gas) } + /// Checks to see if transferObjects is used on GasCoin fn is_gascoin_transfer(tx: SuiTransactionBlockKind) -> bool { match tx { SuiTransactionBlockKind::ProgrammableTransaction(pt) => { @@ -566,36 +567,38 @@ impl Operations { inputs: _, commands, } = &pt; - return commands - .into_iter() - .find(|command| match command { - SuiCommand::TransferObjects(objs, _) => { - objs.iter().any(|&obj| obj == SuiArgument::GasCoin) - } - _ => false, - }) - .is_some(); + return commands.iter().any(|command| match command { + SuiCommand::TransferObjects(objs, _) => { + objs.iter().any(|&obj| obj == SuiArgument::GasCoin) + } + _ => false, + }); } _ => {} } false } + /// If GasCoin is transferred as a part of transferObjects, operations need to be + /// updated such that: + /// 1) gas owner needs to be assigned back to the previous owner + /// 2) SuiBalanceChange type needs to be converted to PaySui for + /// previous and new gas owners and their balances need to be adjusted for the gas fn process_gascoin_transfer( coin_change_operations: &mut impl Iterator, tx: SuiTransactionBlockKind, prev_gas_owner: SuiAddress, new_gas_owner: SuiAddress, gas_used: i128, - ) -> Vec { - let mut operations = vec![]; + ) -> Result, anyhow::Error> { + let mut gascoin_transfer_operations = vec![]; if Self::is_gascoin_transfer(tx) { coin_change_operations.into_iter().for_each(|operation| { match operation.type_ { OperationType::Gas => { // change gas account back to the previous owner as it is the one // who paid for the txn (this is the format Rosetta wants to process) - operations.push(Operation::gas(prev_gas_owner, gas_used)) + gascoin_transfer_operations.push(Operation::gas(prev_gas_owner, gas_used)) } OperationType::SuiBalanceChange => { operation.account.map(|account| { @@ -614,13 +617,13 @@ impl Operations { is_convert_to_pay_sui = true; } if is_convert_to_pay_sui { - operations.push(Operation::pay_sui( + gascoin_transfer_operations.push(Operation::pay_sui( operation.status, account.address, amount.value, )); } else { - operations.push(Operation::balance_change( + gascoin_transfer_operations.push(Operation::balance_change( operation.status, account.address, amount.value, @@ -632,8 +635,15 @@ impl Operations { _ => {} } }); + // sanity check to make sure all the operations have been processed + if coin_change_operations.count() != 0 { + return Err(anyhow!( + "Unable to process all balance-change operations. Remaining count({})", + coin_change_operations.count() + )); + } } - operations + Ok(gascoin_transfer_operations) } } @@ -747,13 +757,15 @@ impl Operations { accounted_balances.clone(), ); + // Take {gas, previous gas owner, new gas owner} out of coin_change_operations + // and convert BalanceChange to PaySui when GasCoin is transferred let gascoin_transfer_operations = Self::process_gascoin_transfer( &mut coin_change_operations, tx.data.transaction().clone(), tx.data.gas_data().owner, gas_owner, gas_used, - ); + )?; let ops: Operations = ops .into_iter() From 69e3c59df43db9861be287ee81a5e19260fd15f8 Mon Sep 17 00:00:00 2001 From: nikos-terzo Date: Wed, 18 Dec 2024 12:56:31 +0200 Subject: [PATCH 6/8] Test parsing of tx with custom coin balance changes --- .../sui-rosetta/tests/custom_coins_tests.rs | 158 +++++++++++++++++- 1 file changed, 153 insertions(+), 5 deletions(-) diff --git a/crates/sui-rosetta/tests/custom_coins_tests.rs b/crates/sui-rosetta/tests/custom_coins_tests.rs index 77094e35e9d7d..419c4fb028777 100644 --- a/crates/sui-rosetta/tests/custom_coins_tests.rs +++ b/crates/sui-rosetta/tests/custom_coins_tests.rs @@ -6,23 +6,36 @@ mod rosetta_client; #[path = "custom_coins/test_coin_utils.rs"] mod test_coin_utils; -use serde_json::json; use std::num::NonZeroUsize; use std::path::Path; +use std::str::FromStr; + +use serde_json::json; + +use shared_crypto::intent::Intent; use sui_json_rpc_types::{ - SuiExecutionStatus, SuiTransactionBlockEffectsAPI, SuiTransactionBlockResponseOptions, + ObjectChange, SuiExecutionStatus, SuiTransactionBlockEffectsAPI, + SuiTransactionBlockResponseOptions, }; +use sui_keys::keystore::AccountKeystore; use sui_rosetta::operations::Operations; use sui_rosetta::types::{ - AccountBalanceRequest, AccountBalanceResponse, AccountIdentifier, Currency, CurrencyMetadata, - NetworkIdentifier, SuiEnv, + AccountBalanceRequest, AccountBalanceResponse, AccountIdentifier, Amount, Currency, + CurrencyMetadata, NetworkIdentifier, SuiEnv, }; use sui_rosetta::types::{Currencies, OperationType}; use sui_rosetta::CoinMetadataCache; use sui_rosetta::SUI; +use sui_types::coin::COIN_MODULE_NAME; +use sui_types::programmable_transaction_builder::ProgrammableTransactionBuilder; +use sui_types::quorum_driver_types::ExecuteTransactionRequestType; +use sui_types::transaction::{ + Argument, Command, ObjectArg, Transaction, TransactionData, TransactionDataAPI, +}; +use sui_types::{Identifier, SUI_FRAMEWORK_PACKAGE_ID}; + use test_cluster::TestClusterBuilder; use test_coin_utils::{init_package, mint}; - use crate::rosetta_client::{start_rosetta_test_server, RosettaEndpoint}; #[tokio::test] @@ -301,3 +314,138 @@ async fn test_custom_coin_without_symbol() { } } } + +#[tokio::test] +async fn test_mint_with_gas_coin_transfer() -> anyhow::Result<()> { + const COIN1_BALANCE: u64 = 100_000_000; + const COIN2_BALANCE: u64 = 200_000_000; + let test_cluster = TestClusterBuilder::new().build().await; + let client = test_cluster.wallet.get_client().await.unwrap(); + let keystore = &test_cluster.wallet.config.keystore; + + let sender = test_cluster.get_address_0(); + let init_ret = init_package( + &client, + keystore, + sender, + Path::new("tests/custom_coins/test_coin"), + ) + .await + .unwrap(); + + let address1 = test_cluster.get_address_1(); + let address2 = test_cluster.get_address_2(); + let balances_to = vec![(COIN1_BALANCE, address1), (COIN2_BALANCE, address2)]; + + let treasury_cap_owner = init_ret.owner; + + let gas_price = client + .governance_api() + .get_reference_gas_price() + .await + .unwrap(); + const LARGE_GAS_BUDGET: u128 = 1_000_000_000; + let mut gas_coins = client + .coin_read_api() + .select_coins(sender, None, LARGE_GAS_BUDGET, vec![]) + .await?; + assert!( + gas_coins.len() == 1, + "Expected 1 large gas-coin to satisfy the budget" + ); + let gas_coin = gas_coins.pop().unwrap(); + let mut ptb = ProgrammableTransactionBuilder::new(); + + let treasury_cap = ptb.obj(ObjectArg::ImmOrOwnedObject(init_ret.treasury_cap))?; + for (balance, to) in balances_to { + let balance = ptb.pure(balance)?; + let coin = ptb.command(Command::move_call( + SUI_FRAMEWORK_PACKAGE_ID, + Identifier::from(COIN_MODULE_NAME), + Identifier::from_str("mint")?, + vec![init_ret.coin_tag.clone()], + vec![treasury_cap, balance], + )); + ptb.transfer_arg(to, coin); + } + ptb.transfer_arg(address1, Argument::GasCoin); + let builder = ptb.finish(); + + // Sign transaction + let tx_data = TransactionData::new_programmable( + treasury_cap_owner, + vec![gas_coin.object_ref()], + builder, + LARGE_GAS_BUDGET as u64, + gas_price, + ); + + let sig = keystore.sign_secure(&tx_data.sender(), &tx_data, Intent::sui_transaction())?; + + let mint_res = client + .quorum_driver_api() + .execute_transaction_block( + Transaction::from_data(tx_data, vec![sig]), + SuiTransactionBlockResponseOptions::new() + .with_balance_changes() + .with_effects() + .with_input() + .with_object_changes(), + Some(ExecuteTransactionRequestType::WaitForLocalExecution), + ) + .await?; + let gas_used = mint_res.effects.as_ref().unwrap().gas_cost_summary(); + let mut gas_used = gas_used.net_gas_usage() as i128; + println!("gas_used: {gas_used}"); + + let coins = mint_res + .object_changes + .as_ref() + .unwrap() + .iter() + .filter_map(|change| { + if let ObjectChange::Created { + object_type, owner, .. + } = change + { + Some((object_type, owner)) + } else { + None + } + }) + .collect::>(); + let coin1 = coins + .iter() + .find(|coin| coin.1.get_address_owner_address().unwrap() == address1) + .unwrap(); + let coin2 = coins + .iter() + .find(|coin| coin.1.get_address_owner_address().unwrap() == address2) + .unwrap(); + assert!(coin1.0.to_string().contains("::test_coin::TEST_COIN")); + assert!(coin2.0.to_string().contains("::test_coin::TEST_COIN")); + + let coin_cache = CoinMetadataCache::new(client, NonZeroUsize::new(2).unwrap()); + let ops = Operations::try_from_response(mint_res, &coin_cache) + .await + .unwrap(); + const COIN_BALANCE_CREATED: u64 = COIN1_BALANCE + COIN2_BALANCE; + println!("ops: {}", serde_json::to_string_pretty(&ops).unwrap()); + let mut coin_created = 0; + ops.into_iter().for_each(|op| { + if let Some(Amount { + value, currency, .. + }) = op.amount + { + if currency == Currency::default() { + gas_used += value + } else { + coin_created += value + } + } + }); + assert!(COIN_BALANCE_CREATED as i128 == coin_created); + assert!(gas_used == 0); + + Ok(()) +} From f41338102f98bd90cedd86bbbf51410bed339ae6 Mon Sep 17 00:00:00 2001 From: Sang Kim Date: Wed, 18 Dec 2024 20:42:21 +0900 Subject: [PATCH 7/8] [Rosetta] fix: Make sure to check currency is SUI when adjusting amount by gas --- crates/sui-rosetta/src/operations.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/sui-rosetta/src/operations.rs b/crates/sui-rosetta/src/operations.rs index 7d9f8ef858786..e3930c25f35c8 100644 --- a/crates/sui-rosetta/src/operations.rs +++ b/crates/sui-rosetta/src/operations.rs @@ -607,11 +607,11 @@ impl Operations { None => return, }; let mut is_convert_to_pay_sui = false; - if account.address == prev_gas_owner { + if account.address == prev_gas_owner && amount.currency == *SUI { // previous owner's balance needs to be adjusted for gas amount.value -= gas_used; is_convert_to_pay_sui = true; - } else if account.address == new_gas_owner { + } else if account.address == new_gas_owner && amount.currency == *SUI { // new owner's balance needs to be adjusted for gas amount.value += gas_used; is_convert_to_pay_sui = true; From 79036707f73604aa1b7c48ecc4c30321fb18a314 Mon Sep 17 00:00:00 2001 From: Sang Kim Date: Fri, 3 Jan 2025 15:07:04 +0900 Subject: [PATCH 8/8] [Rosetta] refactor: Return an Error to stop the parsing rather than quietly discarding operations --- crates/sui-rosetta/src/operations.rs | 76 ++++++++++++++-------------- 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/crates/sui-rosetta/src/operations.rs b/crates/sui-rosetta/src/operations.rs index e3930c25f35c8..54cc9d8a340d7 100644 --- a/crates/sui-rosetta/src/operations.rs +++ b/crates/sui-rosetta/src/operations.rs @@ -593,7 +593,7 @@ impl Operations { ) -> Result, anyhow::Error> { let mut gascoin_transfer_operations = vec![]; if Self::is_gascoin_transfer(tx) { - coin_change_operations.into_iter().for_each(|operation| { + for operation in coin_change_operations.into_iter() { match operation.type_ { OperationType::Gas => { // change gas account back to the previous owner as it is the one @@ -601,46 +601,44 @@ impl Operations { gascoin_transfer_operations.push(Operation::gas(prev_gas_owner, gas_used)) } OperationType::SuiBalanceChange => { - operation.account.map(|account| { - let mut amount = match operation.amount { - Some(amount) => amount, - None => return, - }; - let mut is_convert_to_pay_sui = false; - if account.address == prev_gas_owner && amount.currency == *SUI { - // previous owner's balance needs to be adjusted for gas - amount.value -= gas_used; - is_convert_to_pay_sui = true; - } else if account.address == new_gas_owner && amount.currency == *SUI { - // new owner's balance needs to be adjusted for gas - amount.value += gas_used; - is_convert_to_pay_sui = true; - } - if is_convert_to_pay_sui { - gascoin_transfer_operations.push(Operation::pay_sui( - operation.status, - account.address, - amount.value, - )); - } else { - gascoin_transfer_operations.push(Operation::balance_change( - operation.status, - account.address, - amount.value, - amount.currency, - )); - } - }); + let account = operation + .account + .ok_or_else(|| anyhow!("Missing account for a balance-change"))?; + let mut amount = operation + .amount + .ok_or_else(|| anyhow!("Missing amount for a balance-change"))?; + let mut is_convert_to_pay_sui = false; + if account.address == prev_gas_owner && amount.currency == *SUI { + // previous owner's balance needs to be adjusted for gas + amount.value -= gas_used; + is_convert_to_pay_sui = true; + } else if account.address == new_gas_owner && amount.currency == *SUI { + // new owner's balance needs to be adjusted for gas + amount.value += gas_used; + is_convert_to_pay_sui = true; + } + if is_convert_to_pay_sui { + gascoin_transfer_operations.push(Operation::pay_sui( + operation.status, + account.address, + amount.value, + )); + } else { + gascoin_transfer_operations.push(Operation::balance_change( + operation.status, + account.address, + amount.value, + amount.currency, + )); + } + } + _ => { + return Err(anyhow!( + "Discarding unsupported operation type {:?}", + operation.type_ + )) } - _ => {} } - }); - // sanity check to make sure all the operations have been processed - if coin_change_operations.count() != 0 { - return Err(anyhow!( - "Unable to process all balance-change operations. Remaining count({})", - coin_change_operations.count() - )); } } Ok(gascoin_transfer_operations)