From 7c36a90043e842ab1709c58278426cbf9841ae92 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Fri, 3 Jul 2026 17:53:33 +0900 Subject: [PATCH] impl balance change for sui --- .../crates/gem_evm/src/provider/simulation.rs | 4 +- .../gem_evm/src/provider/simulation_mapper.rs | 25 +-- core/crates/gem_sui/src/models/transaction.rs | 3 + core/crates/gem_sui/src/provider/accounts.rs | 5 +- core/crates/gem_sui/src/provider/mod.rs | 2 + core/crates/gem_sui/src/provider/preload.rs | 2 +- .../crates/gem_sui/src/provider/simulation.rs | 72 ++++++++ .../gem_sui/src/provider/simulation_mapper.rs | 166 ++++++++++++++++++ core/crates/gem_sui/src/provider/testkit.rs | 4 +- .../src/provider/transaction_state_mapper.rs | 10 +- .../src/provider/transactions_mapper.rs | 8 +- core/crates/gem_sui/src/rpc/client.rs | 18 +- core/crates/gem_sui/src/rpc/mapper.rs | 28 ++- core/crates/gem_sui/src/rpc/proto/mod.rs | 2 + core/crates/gem_sui/src/rpc/proto/testkit.rs | 26 +++ .../gem_sui/src/rpc/proto/transactions.rs | 20 +++ core/crates/primitives/src/simulation.rs | 11 +- .../gemstone/src/wallet_connect/simulation.rs | 4 +- .../src/wallet_connect/simulation_client.rs | 22 ++- 19 files changed, 366 insertions(+), 66 deletions(-) create mode 100644 core/crates/gem_sui/src/provider/simulation.rs create mode 100644 core/crates/gem_sui/src/provider/simulation_mapper.rs create mode 100644 core/crates/gem_sui/src/rpc/proto/testkit.rs diff --git a/core/crates/gem_evm/src/provider/simulation.rs b/core/crates/gem_evm/src/provider/simulation.rs index 409fa0454e..936436d116 100644 --- a/core/crates/gem_evm/src/provider/simulation.rs +++ b/core/crates/gem_evm/src/provider/simulation.rs @@ -9,7 +9,7 @@ use gem_client::Client; use primitives::{Asset, SimulationBalanceChange, SimulationInput, SimulationResult}; use crate::jsonrpc::TransactionObject; -use crate::provider::simulation_mapper::{map_balance_change_asset, map_simulation_result}; +use crate::provider::simulation_mapper::map_simulation_result; use crate::rpc::client::EthereumClient; #[async_trait] @@ -31,7 +31,7 @@ impl ChainSimulation for EthereumClient { .into_iter() .zip(assets) .map(|(change, asset)| match asset { - Some(asset) => map_balance_change_asset(change, asset), + Some(asset) => change.with_asset(asset), None => change, }) .collect(); diff --git a/core/crates/gem_evm/src/provider/simulation_mapper.rs b/core/crates/gem_evm/src/provider/simulation_mapper.rs index b5f084ca06..61e18e80ad 100644 --- a/core/crates/gem_evm/src/provider/simulation_mapper.rs +++ b/core/crates/gem_evm/src/provider/simulation_mapper.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use num_bigint::BigInt; use num_traits::Zero; -use primitives::{Asset, AssetId, Chain, SimulationBalanceChange, SimulationResult, SimulationWarning}; +use primitives::{AssetId, Chain, SimulationBalanceChange, SimulationResult, SimulationWarning}; use crate::ethereum_address_checksum; use crate::provider::transfer_decoder::decode_transfer_action; @@ -19,15 +19,6 @@ pub fn map_simulation_result(chain: Chain, signer: &str, trace: &TraceCallResult } } -pub fn map_balance_change_asset(change: SimulationBalanceChange, asset: Asset) -> SimulationBalanceChange { - SimulationBalanceChange { - name: Some(asset.name), - symbol: Some(asset.symbol), - decimals: asset.decimals, - ..change - } -} - fn map_balance_changes(chain: Chain, signer: &str, trace: &TraceCallResult) -> Vec { let mut changes = Vec::new(); @@ -76,7 +67,7 @@ mod tests { use super::*; use alloy_primitives::{Address, U160, U256, address}; use alloy_sol_types::SolCall; - use primitives::asset_constants::{ETHEREUM_USDC_ASSET_ID, ETHEREUM_USDC_TOKEN_ID}; + use primitives::asset_constants::ETHEREUM_USDC_TOKEN_ID; use primitives::contract_constants::UNISWAP_PERMIT2_CONTRACT; use primitives::testkit::json_rpc::load_json_rpc_result; use primitives::testkit::signer_mock::TEST_EVM_RECIPIENT; @@ -186,16 +177,4 @@ mod tests { assert_eq!(map_simulation_result(Chain::Ethereum, TEST_ADDRESS, &trace).balance_changes, vec![]); } - - #[test] - fn test_map_balance_change_asset_sets_metadata_and_decimals() { - let change = SimulationBalanceChange::new(ETHEREUM_USDC_ASSET_ID.clone(), BigInt::from(-1_000_000)); - let asset = Asset::mock_ethereum_usdc(); - - let mapped = map_balance_change_asset(change, asset); - - assert_eq!(mapped.name, Some("USD Coin".to_string())); - assert_eq!(mapped.symbol, Some("USDC".to_string())); - assert_eq!(mapped.decimals, 6); - } } diff --git a/core/crates/gem_sui/src/models/transaction.rs b/core/crates/gem_sui/src/models/transaction.rs index cea3d17a36..64e007bd4a 100644 --- a/core/crates/gem_sui/src/models/transaction.rs +++ b/core/crates/gem_sui/src/models/transaction.rs @@ -14,6 +14,9 @@ use super::account::GasObject; #[cfg(feature = "rpc")] use super::coin::BalanceChange; +pub const STATUS_SUCCESS: &str = "success"; +pub const STATUS_FAILURE: &str = "failure"; + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SuiTransaction { diff --git a/core/crates/gem_sui/src/provider/accounts.rs b/core/crates/gem_sui/src/provider/accounts.rs index 3bb48cecc9..357e6d78a1 100644 --- a/core/crates/gem_sui/src/provider/accounts.rs +++ b/core/crates/gem_sui/src/provider/accounts.rs @@ -1,5 +1,5 @@ #[cfg(feature = "rpc")] -use chain_traits::{ChainAccount, ChainAddressStatus, ChainPerpetual, ChainProvider, ChainSimulation, ChainTraits}; +use chain_traits::{ChainAccount, ChainAddressStatus, ChainPerpetual, ChainProvider, ChainTraits}; use primitives::Chain; use crate::rpc::client::SuiClient; @@ -22,6 +22,3 @@ impl ChainPerpetual for SuiClient {} #[cfg(feature = "rpc")] impl ChainAddressStatus for SuiClient {} - -#[cfg(feature = "rpc")] -impl ChainSimulation for SuiClient {} diff --git a/core/crates/gem_sui/src/provider/mod.rs b/core/crates/gem_sui/src/provider/mod.rs index 35cd85a93e..aa1a48d09c 100644 --- a/core/crates/gem_sui/src/provider/mod.rs +++ b/core/crates/gem_sui/src/provider/mod.rs @@ -4,6 +4,8 @@ pub mod balances_mapper; pub mod preload; pub mod preload_mapper; pub mod request_classifier; +pub mod simulation; +pub mod simulation_mapper; pub mod staking; pub mod staking_mapper; pub mod state; diff --git a/core/crates/gem_sui/src/provider/preload.rs b/core/crates/gem_sui/src/provider/preload.rs index 56ed0feab9..d67329d7a4 100644 --- a/core/crates/gem_sui/src/provider/preload.rs +++ b/core/crates/gem_sui/src/provider/preload.rs @@ -144,7 +144,7 @@ mod chain_integration_tests { async fn test_sui_get_transaction_preload_unstake() -> Result<(), Box> { let client = create_sui_test_client(); - let user_address = "0x93f65b8c16c263343bbf66cf9f8eef69cb1dbc92d13f0c331b0dcaeb76b4aab6"; + let user_address = TEST_ADDRESS; let delegation_id = client .get_stake_delegations(user_address.to_string()) .await? diff --git a/core/crates/gem_sui/src/provider/simulation.rs b/core/crates/gem_sui/src/provider/simulation.rs new file mode 100644 index 0000000000..3df338bc0a --- /dev/null +++ b/core/crates/gem_sui/src/provider/simulation.rs @@ -0,0 +1,72 @@ +use std::error::Error; + +use async_trait::async_trait; +use chain_traits::{ChainSimulation, ChainToken}; +use futures::future::join_all; +use primitives::{Asset, Chain, SimulationBalanceChange, SimulationInput, SimulationResult}; + +use crate::decode_transaction; +use crate::provider::simulation_mapper::map_simulation_result; +use crate::rpc::client::SuiClient; + +#[async_trait] +impl ChainSimulation for SuiClient { + async fn simulate_transaction(&self, input: SimulationInput) -> Result> { + let transaction: sui_types::Transaction = decode_transaction(&input.encoded_transaction).map_err(|err| format!("parse transaction: {err}"))?; + let sender = transaction.sender.to_string(); + + let simulated = self.simulate_encoded_transaction(&input.encoded_transaction).await?; + let mut result = map_simulation_result(&sender, &simulated); + + let changes = std::mem::take(&mut result.balance_changes); + let assets = self.get_balance_change_assets(&changes).await; + result.balance_changes = changes + .into_iter() + .zip(assets) + .map(|(change, asset)| match asset { + Some(asset) => change.with_asset(asset), + None => change, + }) + .collect(); + + Ok(result) + } +} + +impl SuiClient { + async fn get_balance_change_assets(&self, changes: &[SimulationBalanceChange]) -> Vec> { + join_all(changes.iter().map(|change| async move { + match &change.asset_id.token_id { + None => Some(Asset::from_chain(Chain::Sui)), + Some(coin_type) => self.get_token_data(coin_type.clone()).await.ok(), + } + })) + .await + } +} + +#[cfg(all(test, feature = "chain_integration_tests"))] +mod chain_integration_tests { + use super::*; + use crate::provider::testkit::{TEST_ADDRESS, TEST_ADDRESS_EMPTY, create_sui_test_client}; + use crate::transfer_builder::build_transfer_message_bytes; + use primitives::AssetId; + + #[tokio::test] + async fn test_simulate_transaction_native_transfer() -> Result<(), Box> { + let client = create_sui_test_client(); + let encoded_transaction = build_transfer_message_bytes(&client, TEST_ADDRESS, TEST_ADDRESS_EMPTY, 100, None).await?; + + let result = client.simulate_transaction(SimulationInput { encoded_transaction }).await?; + + assert!(result.warnings.is_empty()); + let change = result + .balance_changes + .iter() + .find(|change| change.asset_id == AssetId::from_chain(Chain::Sui)) + .ok_or("missing sender SUI balance change")?; + assert!(change.value.starts_with('-')); + assert_eq!(change.symbol.as_deref(), Some("SUI")); + Ok(()) + } +} diff --git a/core/crates/gem_sui/src/provider/simulation_mapper.rs b/core/crates/gem_sui/src/provider/simulation_mapper.rs new file mode 100644 index 0000000000..0ef77b65a3 --- /dev/null +++ b/core/crates/gem_sui/src/provider/simulation_mapper.rs @@ -0,0 +1,166 @@ +use std::str::FromStr; + +use num_bigint::BigInt; +use num_traits::Zero; +use primitives::{SimulationBalanceChange, SimulationResult, SimulationWarning}; + +use crate::provider::transactions_mapper::map_asset_id; +use crate::rpc::proto::{BalanceChange, ExecutedTransaction}; + +pub fn map_simulation_result(sender: &str, transaction: &ExecutedTransaction) -> SimulationResult { + if !transaction.execution_success() { + let message = transaction.execution_error().unwrap_or_else(|| "execution failed".to_string()); + return SimulationResult::new(vec![SimulationWarning::validation_error(message)], vec![]); + } + + match map_balance_changes(sender, &transaction.balance_changes) { + Ok(balance_changes) => SimulationResult { + balance_changes, + ..Default::default() + }, + Err(message) => SimulationResult::new(vec![SimulationWarning::validation_error(message)], vec![]), + } +} + +fn map_balance_changes(sender: &str, balance_changes: &[BalanceChange]) -> Result, &'static str> { + let mut changes = Vec::new(); + for change in balance_changes.iter().filter(|change| change.address.as_deref() == Some(sender)) { + let amount = change + .amount + .as_deref() + .and_then(|amount| BigInt::from_str(amount).ok()) + .ok_or("missing or malformed balance change amount")?; + if amount.is_zero() { + continue; + } + let coin_type = change.coin_type.as_deref().ok_or("missing balance change coin type")?; + changes.push(SimulationBalanceChange::new(map_asset_id(coin_type), amount)); + } + changes.sort_by_key(|change| change.asset_id.to_string()); + Ok(changes) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::SUI_COIN_TYPE_FULL; + use crate::provider::testkit::{TEST_ADDRESS, TEST_ADDRESS_EMPTY}; + use crate::rpc::proto::TransactionEffects; + use primitives::asset_constants::SUI_USDC_TOKEN_ID; + use primitives::{AssetId, Chain}; + + #[test] + fn test_map_simulation_result() { + let transaction = ExecutedTransaction { + effects: Some(TransactionEffects::mock(true, None)), + balance_changes: vec![ + BalanceChange::mock(TEST_ADDRESS, SUI_COIN_TYPE_FULL, "-101744880"), + BalanceChange::mock(TEST_ADDRESS, SUI_USDC_TOKEN_ID, "250000"), + BalanceChange::mock(TEST_ADDRESS, SUI_USDC_TOKEN_ID, "0"), + BalanceChange::mock(TEST_ADDRESS_EMPTY, SUI_COIN_TYPE_FULL, "100000000"), + ], + ..Default::default() + }; + + let result = map_simulation_result(TEST_ADDRESS, &transaction); + + assert!(result.warnings.is_empty()); + assert_eq!( + result.balance_changes, + vec![ + SimulationBalanceChange::new(AssetId::from_chain(Chain::Sui), BigInt::from(-101_744_880)), + SimulationBalanceChange::new(AssetId::from_token(Chain::Sui, SUI_USDC_TOKEN_ID), BigInt::from(250_000)), + ] + ); + } + + #[test] + fn test_map_simulation_result_malformed_signer_change_returns_validation_warning() { + let cases = [ + ( + BalanceChange { + address: Some(TEST_ADDRESS.to_string()), + coin_type: Some(SUI_COIN_TYPE_FULL.to_string()), + amount: None, + }, + "missing or malformed balance change amount", + ), + ( + BalanceChange { + address: Some(TEST_ADDRESS.to_string()), + coin_type: Some(SUI_COIN_TYPE_FULL.to_string()), + amount: Some("not-a-number".to_string()), + }, + "missing or malformed balance change amount", + ), + ( + BalanceChange { + address: Some(TEST_ADDRESS.to_string()), + coin_type: None, + amount: Some("100".to_string()), + }, + "missing balance change coin type", + ), + ]; + + for (change, message) in cases { + let transaction = ExecutedTransaction { + effects: Some(TransactionEffects::mock(true, None)), + balance_changes: vec![BalanceChange::mock(TEST_ADDRESS, SUI_COIN_TYPE_FULL, "-100"), change], + ..Default::default() + }; + + let result = map_simulation_result(TEST_ADDRESS, &transaction); + + assert_eq!(result.warnings, vec![SimulationWarning::validation_error(message)]); + assert!(result.balance_changes.is_empty()); + } + + // zero amounts stay skipped even without a coin type, and malformed changes of other addresses stay ignored + let transaction = ExecutedTransaction { + effects: Some(TransactionEffects::mock(true, None)), + balance_changes: vec![ + BalanceChange { + address: Some(TEST_ADDRESS.to_string()), + coin_type: None, + amount: Some("0".to_string()), + }, + BalanceChange { + address: Some(TEST_ADDRESS_EMPTY.to_string()), + coin_type: None, + amount: None, + }, + BalanceChange::mock(TEST_ADDRESS, SUI_COIN_TYPE_FULL, "-100"), + ], + ..Default::default() + }; + + let result = map_simulation_result(TEST_ADDRESS, &transaction); + + assert!(result.warnings.is_empty()); + assert_eq!( + result.balance_changes, + vec![SimulationBalanceChange::new(AssetId::from_chain(Chain::Sui), BigInt::from(-100))] + ); + } + + #[test] + fn test_map_simulation_result_failed_execution_returns_validation_warning() { + let transaction = ExecutedTransaction { + effects: Some(TransactionEffects::mock(false, Some("InsufficientGas"))), + balance_changes: vec![BalanceChange::mock(TEST_ADDRESS, SUI_COIN_TYPE_FULL, "-101744880")], + ..Default::default() + }; + + let result = map_simulation_result(TEST_ADDRESS, &transaction); + + assert_eq!(result.warnings, vec![SimulationWarning::validation_error("InsufficientGas")]); + assert!(result.balance_changes.is_empty()); + + let missing_status = ExecutedTransaction::default(); + assert_eq!( + map_simulation_result(TEST_ADDRESS, &missing_status).warnings, + vec![SimulationWarning::validation_error("execution failed")] + ); + } +} diff --git a/core/crates/gem_sui/src/provider/testkit.rs b/core/crates/gem_sui/src/provider/testkit.rs index 1dee1e6592..55e7ba57b4 100644 --- a/core/crates/gem_sui/src/provider/testkit.rs +++ b/core/crates/gem_sui/src/provider/testkit.rs @@ -5,9 +5,9 @@ use primitives::asset_constants::SUI_USDC_TOKEN_ID; #[cfg(all(test, feature = "chain_integration_tests"))] use settings::testkit::get_test_settings; -#[cfg(all(test, feature = "chain_integration_tests"))] +#[cfg(test)] pub const TEST_ADDRESS: &str = "0x93f65b8c16c263343bbf66cf9f8eef69cb1dbc92d13f0c331b0dcaeb76b4aab6"; -#[cfg(all(test, feature = "chain_integration_tests"))] +#[cfg(test)] pub const TEST_ADDRESS_EMPTY: &str = "0x180c5478e639770c4424bfbcd4208d8d61f4e52518c76c5ea1ed05b418380457"; #[cfg(all(test, feature = "chain_integration_tests"))] diff --git a/core/crates/gem_sui/src/provider/transaction_state_mapper.rs b/core/crates/gem_sui/src/provider/transaction_state_mapper.rs index cac685fc1b..ae9c17e01e 100644 --- a/core/crates/gem_sui/src/provider/transaction_state_mapper.rs +++ b/core/crates/gem_sui/src/provider/transaction_state_mapper.rs @@ -1,11 +1,11 @@ use primitives::{TransactionState, TransactionUpdate}; -use crate::models::Digest; +use crate::models::{Digest, STATUS_FAILURE, STATUS_SUCCESS}; pub fn map_transaction_status(transaction: Digest) -> TransactionUpdate { let state = match transaction.effects.status.status.as_str() { - "success" => TransactionState::Confirmed, - "failure" => TransactionState::Reverted, + STATUS_SUCCESS => TransactionState::Confirmed, + STATUS_FAILURE => TransactionState::Reverted, _ => TransactionState::Pending, }; TransactionUpdate::new_state(state) @@ -28,7 +28,9 @@ mod tests { storage_rebate: BigUint::from(100u32), non_refundable_storage_fee: BigUint::from(0u32), }, - status: Status { status: "success".to_string() }, + status: Status { + status: STATUS_SUCCESS.to_string(), + }, gas_object: GasObject { owner: Owner::String("0x123".to_string()), }, diff --git a/core/crates/gem_sui/src/provider/transactions_mapper.rs b/core/crates/gem_sui/src/provider/transactions_mapper.rs index 762f718b0a..68d1bfbc53 100644 --- a/core/crates/gem_sui/src/provider/transactions_mapper.rs +++ b/core/crates/gem_sui/src/provider/transactions_mapper.rs @@ -1,4 +1,4 @@ -use crate::models::{BalanceChange, Digest, Event, EventStake, EventUnstake, GasUsed, TransactionBlocks}; +use crate::models::{BalanceChange, Digest, Event, EventStake, EventUnstake, GasUsed, STATUS_SUCCESS, TransactionBlocks}; use crate::{SUI_COIN_TYPE, SUI_STAKE_EVENT, SUI_UNSTAKE_EVENT, full_coin_type, sui_framework_package_address}; use chain_primitives::{BalanceDiff, SwapMapper}; use chrono::{TimeZone, Utc}; @@ -26,7 +26,7 @@ pub fn map_transaction(transaction: Digest) -> Option { let hash = transaction.digest.clone(); let fee = get_fee(effects.gas_used.clone()); let created_at = Utc.timestamp_millis_opt(transaction.timestamp_ms as i64).unwrap(); - let state = if effects.status.status == "success" { + let state = if effects.status.status == STATUS_SUCCESS { TransactionState::Confirmed } else { TransactionState::Failed @@ -292,7 +292,9 @@ mod tests { storage_rebate: BigUint::from(0u32), non_refundable_storage_fee: BigUint::from(0u32), }, - status: Status { status: "success".to_string() }, + status: Status { + status: STATUS_SUCCESS.to_string(), + }, gas_object: GasObject { owner: owner(OWNER_ADDRESS) }, }, move_call_packages: Vec::new(), diff --git a/core/crates/gem_sui/src/rpc/client.rs b/core/crates/gem_sui/src/rpc/client.rs index 55d0f09006..4058581010 100644 --- a/core/crates/gem_sui/src/rpc/client.rs +++ b/core/crates/gem_sui/src/rpc/client.rs @@ -11,10 +11,10 @@ use sui_types::Address; use super::mapper::{map_checkpoint, map_executed_transaction, map_inspect_result, map_sui_effects}; use super::proto::{ self as proto, BatchGetObjectsRequest, BatchGetObjectsResponse, BatchGetTransactionsRequest, BatchGetTransactionsResponse, ExecuteTransactionRequest, - ExecuteTransactionResponse, FieldMask, GetBalanceRequest, GetBalanceResponse, GetCheckpointRequest, GetCheckpointResponse, GetCoinInfoRequest, GetCoinInfoResponse, - GetEpochRequest, GetEpochResponse, GetFunctionRequest, GetFunctionResponse, GetObjectRequest, GetObjectResponse, GetServiceInfoRequest, GetServiceInfoResponse, - GetTransactionRequest, GetTransactionResponse, ListBalancesRequest, ListBalancesResponse, ListOwnedObjectsRequest, ListOwnedObjectsResponse, SimulateTransactionRequest, - SimulateTransactionResponse, Transaction as GrpcTransaction, TransactionChecks, UserSignature as GrpcUserSignature, WithMut, + ExecuteTransactionResponse, ExecutedTransaction, FieldMask, GetBalanceRequest, GetBalanceResponse, GetCheckpointRequest, GetCheckpointResponse, GetCoinInfoRequest, + GetCoinInfoResponse, GetEpochRequest, GetEpochResponse, GetFunctionRequest, GetFunctionResponse, GetObjectRequest, GetObjectResponse, GetServiceInfoRequest, + GetServiceInfoResponse, GetTransactionRequest, GetTransactionResponse, ListBalancesRequest, ListBalancesResponse, ListOwnedObjectsRequest, ListOwnedObjectsResponse, + SimulateTransactionRequest, SimulateTransactionResponse, Transaction as GrpcTransaction, TransactionChecks, UserSignature as GrpcUserSignature, WithMut, }; use super::transport::default_transport; use crate::models::transaction::{SuiBroadcastTransaction, SuiTransaction}; @@ -270,6 +270,16 @@ impl SuiClient { }) } + pub async fn simulate_encoded_transaction(&self, encoded_transaction: &str) -> Result> { + let transaction = decode_transaction_base64(encoded_transaction)?; + let request = SimulateTransactionRequest::new(transaction).with(|request| { + request.read_mask = Some(FieldMask::from_paths(["transaction.balance_changes", "transaction.effects.status"])); + request.checks = Some(TransactionChecks::Enabled); + }); + let response: SimulateTransactionResponse = self.grpc_unary(PATH_SIMULATE_TRANSACTION, request).await?; + response.transaction.ok_or_else(|| "missing simulated transaction".into()) + } + pub async fn get_transaction(&self, transaction_id: String) -> Result> { let request = GetTransactionRequest { digest: Some(transaction_id), diff --git a/core/crates/gem_sui/src/rpc/mapper.rs b/core/crates/gem_sui/src/rpc/mapper.rs index 0f92521ec6..2bea1ac436 100644 --- a/core/crates/gem_sui/src/rpc/mapper.rs +++ b/core/crates/gem_sui/src/rpc/mapper.rs @@ -3,7 +3,7 @@ use std::{error::Error, str::FromStr}; use num_bigint::{BigInt, BigUint}; use super::proto::{self, Command, OwnerKind, Timestamp}; -use crate::models::transaction::SuiStatus; +use crate::models::transaction::{STATUS_FAILURE, STATUS_SUCCESS, SuiStatus}; use crate::models::{ BalanceChange, Checkpoint, Digest, Effect, Event, GasObject, GasUsed, InspectCommandResult, InspectEffects, InspectGasUsed, InspectResult, Owner, OwnerObject, Status, SuiEffects, @@ -76,9 +76,7 @@ fn map_effect(effects: Option<&proto::TransactionEffects>) -> Effect { Effect { gas_used: map_gas_used(effects.and_then(|effects| effects.gas_used.as_ref())), - status: Status { - status: if transaction_success(effects) { "success" } else { "failure" }.to_string(), - }, + status: Status { status: map_status(effects) }, gas_object: GasObject { owner: gas_object_owner }, } } @@ -87,21 +85,19 @@ pub(super) fn map_sui_effects(effects: Option<&proto::TransactionEffects>) -> Su SuiEffects { gas_used: map_gas_used(effects.and_then(|effects| effects.gas_used.as_ref())), status: SuiStatus { - status: if transaction_success(effects) { "success" } else { "failure" }.to_string(), - error: transaction_error(effects), + status: map_status(effects), + error: effects.and_then(proto::TransactionEffects::execution_error), }, } } -fn transaction_success(effects: Option<&proto::TransactionEffects>) -> bool { - effects.and_then(|effects| effects.status.as_ref()).and_then(|status| status.success).unwrap_or(false) -} - -fn transaction_error(effects: Option<&proto::TransactionEffects>) -> Option { - effects - .and_then(|effects| effects.status.as_ref()) - .and_then(|status| status.error.as_ref()) - .and_then(|error| error.description.clone()) +fn map_status(effects: Option<&proto::TransactionEffects>) -> String { + if effects.is_some_and(proto::TransactionEffects::execution_success) { + STATUS_SUCCESS + } else { + STATUS_FAILURE + } + .to_string() } fn map_gas_used(gas: Option<&proto::GasCostSummary>) -> GasUsed { @@ -155,7 +151,7 @@ pub(super) fn map_inspect_result(response: proto::SimulateTransactionResponse) - }, }, events: serde_json::Value::Null, - error: transaction_error(effects), + error: effects.and_then(proto::TransactionEffects::execution_error), results: response .command_outputs .into_iter() diff --git a/core/crates/gem_sui/src/rpc/proto/mod.rs b/core/crates/gem_sui/src/rpc/proto/mod.rs index 9725f79f61..73011346a8 100644 --- a/core/crates/gem_sui/src/rpc/proto/mod.rs +++ b/core/crates/gem_sui/src/rpc/proto/mod.rs @@ -7,6 +7,8 @@ pub(crate) mod move_package; pub(crate) mod objects; pub(crate) mod service; pub(crate) mod status; +#[cfg(test)] +pub(crate) mod testkit; pub(crate) mod timestamp; pub(crate) mod transaction_data; pub(crate) mod transactions; diff --git a/core/crates/gem_sui/src/rpc/proto/testkit.rs b/core/crates/gem_sui/src/rpc/proto/testkit.rs new file mode 100644 index 0000000000..f67f47a862 --- /dev/null +++ b/core/crates/gem_sui/src/rpc/proto/testkit.rs @@ -0,0 +1,26 @@ +use crate::rpc::proto::transactions::{ExecutionError, ExecutionStatus}; +use crate::rpc::proto::{BalanceChange, TransactionEffects}; + +impl BalanceChange { + pub fn mock(address: &str, coin_type: &str, amount: &str) -> Self { + Self { + address: Some(address.to_string()), + coin_type: Some(coin_type.to_string()), + amount: Some(amount.to_string()), + } + } +} + +impl TransactionEffects { + pub fn mock(success: bool, error: Option<&str>) -> Self { + Self { + status: Some(ExecutionStatus { + success: Some(success), + error: error.map(|description| ExecutionError { + description: Some(description.to_string()), + }), + }), + ..Default::default() + } + } +} diff --git a/core/crates/gem_sui/src/rpc/proto/transactions.rs b/core/crates/gem_sui/src/rpc/proto/transactions.rs index 2b2928cb8c..dc3871b499 100644 --- a/core/crates/gem_sui/src/rpc/proto/transactions.rs +++ b/core/crates/gem_sui/src/rpc/proto/transactions.rs @@ -170,6 +170,16 @@ proto_decode!(ExecutedTransaction { 8 => balance_changes: repeated_message, }); +impl ExecutedTransaction { + pub fn execution_success(&self) -> bool { + self.effects.as_ref().is_some_and(TransactionEffects::execution_success) + } + + pub fn execution_error(&self) -> Option { + self.effects.as_ref()?.execution_error() + } +} + #[derive(Clone, Debug, Default)] pub struct TransactionEffects { pub status: Option, @@ -183,6 +193,16 @@ proto_decode!(TransactionEffects { 8 => gas_object: optional_message, }); +impl TransactionEffects { + pub fn execution_success(&self) -> bool { + self.status.as_ref().and_then(|status| status.success).unwrap_or(false) + } + + pub fn execution_error(&self) -> Option { + self.status.as_ref()?.error.as_ref()?.description.clone() + } +} + #[derive(Clone, Debug, Default)] pub struct ExecutionStatus { pub success: Option, diff --git a/core/crates/primitives/src/simulation.rs b/core/crates/primitives/src/simulation.rs index 48e841163b..9ce6b78f39 100644 --- a/core/crates/primitives/src/simulation.rs +++ b/core/crates/primitives/src/simulation.rs @@ -2,7 +2,7 @@ use num_bigint::BigInt; use serde::{Deserialize, Serialize}; use typeshare::typeshare; -use crate::AssetId; +use crate::{Asset, AssetId}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[typeshare(swift = "Equatable, Hashable, Sendable")] @@ -115,6 +115,15 @@ impl SimulationBalanceChange { symbol: None, } } + + pub fn with_asset(self, asset: Asset) -> Self { + Self { + name: Some(asset.name), + symbol: Some(asset.symbol), + decimals: asset.decimals, + ..self + } + } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/core/gemstone/src/wallet_connect/simulation.rs b/core/gemstone/src/wallet_connect/simulation.rs index afacac9520..26b14d0b68 100644 --- a/core/gemstone/src/wallet_connect/simulation.rs +++ b/core/gemstone/src/wallet_connect/simulation.rs @@ -52,9 +52,10 @@ pub(super) fn decode_ethereum_calldata(transaction: &WcEthereumTransactionData) transaction.data.as_deref().and_then(|calldata| hex::decode_hex(calldata).ok()).unwrap_or_default() } -pub(super) fn decode_solana_transaction(transaction_type: &WcWalletConnectTransactionType, data: &str) -> Option { +pub(super) fn decode_encoded_transaction(transaction_type: &WcWalletConnectTransactionType, data: &str) -> Option { match WalletConnectRequestHandler::decode_send_transaction(transaction_type.clone(), data.to_string()).ok()? { WcWalletConnectTransaction::Solana { data, .. } => Some(data.transaction), + WcWalletConnectTransaction::Sui { data, .. } => Some(data.transaction), _ => None, } } @@ -62,6 +63,7 @@ pub(super) fn decode_solana_transaction(transaction_type: &WcWalletConnectTransa #[cfg(test)] mod tests { use super::*; + use primitives::TransferDataOutputType; #[test] fn decode_ethereum_transaction_with_calldata_decodes_bytes() { diff --git a/core/gemstone/src/wallet_connect/simulation_client.rs b/core/gemstone/src/wallet_connect/simulation_client.rs index 729ee817a6..3904932bb5 100644 --- a/core/gemstone/src/wallet_connect/simulation_client.rs +++ b/core/gemstone/src/wallet_connect/simulation_client.rs @@ -4,7 +4,9 @@ use ::simulation::evm::SimulationClient; use chain_traits::ChainSimulation; use gem_evm::jsonrpc::TransactionObject; use gem_evm::rpc::EthereumClient; +use gem_jsonrpc::grpc::AlienGrpcTransport; use gem_solana::rpc::client::SolanaClient; +use gem_sui::rpc::client::SuiClient; use gem_wallet_connect::{ SignDigestType as WcSignDigestType, WCEthereumTransactionData as WcEthereumTransactionData, WalletConnectTransactionType as WcWalletConnectTransactionType, }; @@ -12,7 +14,7 @@ use primitives::{Chain, EVMChain, SimulationInput, SimulationResult}; use crate::{ GemstoneError, - alien::{AlienClient, AlienProvider, coalescing_provider, new_alien_client}, + alien::{AlienClient, AlienProvider, AlienProviderWrapper, coalescing_provider, new_alien_client}, message::sign_type::SignDigestType, network::JsonRpcClient, }; @@ -54,7 +56,7 @@ impl WalletConnectSimulationClient { let simulation = match &transaction_type { WcWalletConnectTransactionType::Ethereum => self.simulate_ethereum_transaction(chain, &data).await, - WcWalletConnectTransactionType::Solana { .. } => self.simulate_solana_transaction(&transaction_type, &data).await, + WcWalletConnectTransactionType::Solana { .. } | WcWalletConnectTransactionType::Sui { .. } => self.simulate_encoded_transaction(&transaction_type, &data).await, _ => Ok(SimulationResult::default()), } .unwrap_or_default(); @@ -115,9 +117,13 @@ impl WalletConnectSimulationClient { Ok(client.simulate_transaction(SimulationInput { encoded_transaction }).await?) } - async fn simulate_solana_transaction(&self, transaction_type: &WcWalletConnectTransactionType, data: &str) -> Result { - let encoded_transaction = simulation::decode_solana_transaction(transaction_type, data).ok_or("Failed to decode transaction")?; - let client = self.solana_client().ok_or("No RPC client available")?; + async fn simulate_encoded_transaction(&self, transaction_type: &WcWalletConnectTransactionType, data: &str) -> Result { + let encoded_transaction = simulation::decode_encoded_transaction(transaction_type, data).ok_or("Failed to decode transaction")?; + let client: Box = match transaction_type { + WcWalletConnectTransactionType::Solana { .. } => Box::new(self.solana_client().ok_or("No RPC client available")?), + WcWalletConnectTransactionType::Sui { .. } => Box::new(self.sui_client().ok_or("No RPC client available")?), + _ => return Err("Chain does not use encoded transaction simulation".into()), + }; Ok(client.simulate_transaction(SimulationInput { encoded_transaction }).await?) } @@ -133,6 +139,12 @@ impl WalletConnectSimulationClient { let client = new_alien_client(url, self.provider.clone()); Some(SolanaClient::new(JsonRpcClient::new(client))) } + + fn sui_client(&self) -> Option { + let url = self.provider.get_endpoint(Chain::Sui).ok()?; + let transport = AlienGrpcTransport::new(Arc::new(AlienProviderWrapper::new(self.provider.clone()))); + Some(SuiClient::new_with_transport(url, Arc::new(transport))) + } } /// Keeps the gas limit so out-of-gas failures surface, but omits fee prices - they make the trace charge gas and leak fee accounting into the signer's balance diff.