Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions core/crates/gem_evm/src/provider/simulation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -31,7 +31,7 @@ impl<C: Client + Clone> ChainSimulation for EthereumClient<C> {
.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();
Expand Down
25 changes: 2 additions & 23 deletions core/crates/gem_evm/src/provider/simulation_mapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<SimulationBalanceChange> {
let mut changes = Vec::new();

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
3 changes: 3 additions & 0 deletions core/crates/gem_sui/src/models/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 1 addition & 4 deletions core/crates/gem_sui/src/provider/accounts.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -22,6 +22,3 @@ impl ChainPerpetual for SuiClient {}

#[cfg(feature = "rpc")]
impl ChainAddressStatus for SuiClient {}

#[cfg(feature = "rpc")]
impl ChainSimulation for SuiClient {}
2 changes: 2 additions & 0 deletions core/crates/gem_sui/src/provider/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion core/crates/gem_sui/src/provider/preload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ mod chain_integration_tests {
async fn test_sui_get_transaction_preload_unstake() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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?
Expand Down
72 changes: 72 additions & 0 deletions core/crates/gem_sui/src/provider/simulation.rs
Original file line number Diff line number Diff line change
@@ -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<SimulationResult, Box<dyn Error + Send + Sync>> {
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<Option<Asset>> {
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<dyn std::error::Error + Send + Sync>> {
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(())
}
}
166 changes: 166 additions & 0 deletions core/crates/gem_sui/src/provider/simulation_mapper.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<SimulationBalanceChange>, &'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")]
);
}
}
4 changes: 2 additions & 2 deletions core/crates/gem_sui/src/provider/testkit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"))]
Expand Down
10 changes: 6 additions & 4 deletions core/crates/gem_sui/src/provider/transaction_state_mapper.rs
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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()),
},
Expand Down
Loading
Loading