diff --git a/Cargo.lock b/Cargo.lock index 36e0fecb8..ba4e47d3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3130,6 +3130,7 @@ dependencies = [ "chain_primitives", "chain_traits", "chrono", + "futures", "gem_bsc", "gem_client", "gem_hash", diff --git a/apps/api/src/referral/mod.rs b/apps/api/src/referral/mod.rs index 6c15813b2..52d2f01d5 100644 --- a/apps/api/src/referral/mod.rs +++ b/apps/api/src/referral/mod.rs @@ -76,7 +76,7 @@ pub async fn redeem_rewards( request: Authenticated, client: &State>, ) -> Result, ApiError> { - if wallet.0.address() != &request.auth.address { + if wallet.0.address() != request.auth.address { return Err(ApiError::BadRequest("Address mismatch".to_string())); } Ok(client.lock().await.redeem(&wallet.id(), &request.data.id, request.auth.device.id).await?.into()) diff --git a/crates/chain_primitives/src/balance_diff.rs b/crates/chain_primitives/src/balance_diff.rs index 42f25030b..6684a77b2 100644 --- a/crates/chain_primitives/src/balance_diff.rs +++ b/crates/chain_primitives/src/balance_diff.rs @@ -1,18 +1,7 @@ use num_bigint::{BigInt, BigUint}; -use std::collections::HashMap; use primitives::{AssetId, TransactionSwapMetadata}; - -/// Address -> Vec -pub type BalanceDiffMap = HashMap>; - -#[derive(Debug)] -pub struct BalanceDiff { - pub asset_id: AssetId, - pub from_value: Option, - pub to_value: Option, - pub diff: BigInt, -} +pub use primitives::{BalanceDiff, BalanceDiffMap}; pub struct SwapMapper; diff --git a/crates/chain_traits/src/lib.rs b/crates/chain_traits/src/lib.rs index 36520af2f..57c78ef41 100644 --- a/crates/chain_traits/src/lib.rs +++ b/crates/chain_traits/src/lib.rs @@ -5,8 +5,9 @@ use primitives::chart::ChartCandleStick; use primitives::perpetual::{PerpetualData, PerpetualPositionsSummary}; use primitives::portfolio::PerpetualPortfolio; use primitives::{ - AddressStatus, Asset, AssetBalance, BroadcastOptions, Chain, ChartPeriod, DelegationBase, DelegationValidator, FeeRate, NodeSyncStatus, Transaction, TransactionFee, - TransactionInputType, TransactionLoadData, TransactionLoadInput, TransactionLoadMetadata, TransactionPreloadInput, TransactionStateRequest, TransactionUpdate, UTXO, + AddressStatus, Asset, AssetBalance, BroadcastOptions, Chain, ChartPeriod, DelegationBase, DelegationValidator, FeeRate, NodeSyncStatus, SimulationInput, SimulationResult, + Transaction, TransactionFee, TransactionInputType, TransactionLoadData, TransactionLoadInput, TransactionLoadMetadata, TransactionPreloadInput, TransactionStateRequest, + TransactionUpdate, UTXO, }; pub trait ChainTraits: @@ -21,6 +22,7 @@ pub trait ChainTraits: + ChainToken + ChainTransactionLoad + ChainAddressStatus + + ChainSimulation { } @@ -156,3 +158,10 @@ pub trait ChainAddressStatus: Send + Sync { Ok(vec![]) } } + +#[async_trait] +pub trait ChainSimulation: Send + Sync { + async fn simulate_transaction(&self, _input: SimulationInput) -> Result> { + Err("Chain does not support transaction simulation".into()) + } +} diff --git a/crates/gem_algorand/src/rpc/client.rs b/crates/gem_algorand/src/rpc/client.rs index 3c8b407e9..f7fabfa53 100644 --- a/crates/gem_algorand/src/rpc/client.rs +++ b/crates/gem_algorand/src/rpc/client.rs @@ -8,7 +8,7 @@ use crate::{ use gem_client::{CONTENT_TYPE, ContentType}; #[cfg(feature = "rpc")] -use chain_traits::{ChainAccount, ChainAddressStatus, ChainPerpetual, ChainProvider, ChainStaking, ChainTraits}; +use chain_traits::{ChainAccount, ChainAddressStatus, ChainPerpetual, ChainProvider, ChainSimulation, ChainStaking, ChainTraits}; #[cfg(feature = "rpc")] use gem_client::{Client, ClientExt}; #[cfg(feature = "rpc")] @@ -80,5 +80,8 @@ impl ChainPerpetual for AlgorandClient {} #[cfg(feature = "rpc")] impl ChainAddressStatus for AlgorandClient {} +#[cfg(feature = "rpc")] +impl ChainSimulation for AlgorandClient {} + #[cfg(feature = "rpc")] impl ChainTraits for AlgorandClient {} diff --git a/crates/gem_aptos/src/rpc/client.rs b/crates/gem_aptos/src/rpc/client.rs index 6a2475cce..22b389a38 100644 --- a/crates/gem_aptos/src/rpc/client.rs +++ b/crates/gem_aptos/src/rpc/client.rs @@ -221,7 +221,7 @@ impl AptosClient { mod chain_trait_impls { use super::*; use async_trait::async_trait; - use chain_traits::{ChainAccount, ChainAddressStatus, ChainPerpetual}; + use chain_traits::{ChainAccount, ChainAddressStatus, ChainPerpetual, ChainSimulation}; #[async_trait] impl ChainAccount for AptosClient {} @@ -231,4 +231,7 @@ mod chain_trait_impls { #[async_trait] impl ChainAddressStatus for AptosClient {} + + #[async_trait] + impl ChainSimulation for AptosClient {} } diff --git a/crates/gem_bitcoin/src/rpc/client.rs b/crates/gem_bitcoin/src/rpc/client.rs index 996d09231..59571ce66 100644 --- a/crates/gem_bitcoin/src/rpc/client.rs +++ b/crates/gem_bitcoin/src/rpc/client.rs @@ -4,7 +4,7 @@ use crate::models::account::BitcoinAccount; use crate::models::block::{BitcoinBlock, BitcoinNodeInfo, Block, Status}; use crate::models::fee::BitcoinFeeResult; use crate::models::transaction::{AddressDetails, BitcoinTransactionBroacastResult, BitcoinUTXO, Transaction}; -use chain_traits::{ChainAddressStatus, ChainPerpetual, ChainStaking, ChainToken, ChainTraits}; +use chain_traits::{ChainAddressStatus, ChainPerpetual, ChainSimulation, ChainStaking, ChainToken, ChainTraits}; use gem_client::{CONTENT_TYPE, Client, ClientExt, ContentType}; use primitives::{BitcoinChain, chain::Chain}; use std::collections::HashMap; @@ -75,6 +75,8 @@ impl ChainAddressStatus for BitcoinClient {} impl ChainToken for BitcoinClient {} +impl ChainSimulation for BitcoinClient {} + impl ChainTraits for BitcoinClient {} impl chain_traits::ChainProvider for BitcoinClient { diff --git a/crates/gem_cardano/src/rpc/client.rs b/crates/gem_cardano/src/rpc/client.rs index 672af6e73..5ee5a9cd2 100644 --- a/crates/gem_cardano/src/rpc/client.rs +++ b/crates/gem_cardano/src/rpc/client.rs @@ -1,6 +1,6 @@ use std::error::Error; -use chain_traits::{ChainAddressStatus, ChainPerpetual, ChainProvider, ChainStaking, ChainTraits}; +use chain_traits::{ChainAddressStatus, ChainPerpetual, ChainProvider, ChainSimulation, ChainStaking, ChainTraits}; use gem_client::{Client, ClientExt}; use primitives::chain::Chain; @@ -129,6 +129,8 @@ impl ChainPerpetual for CardanoClient {} impl ChainAddressStatus for CardanoClient {} +impl ChainSimulation for CardanoClient {} + impl ChainTraits for CardanoClient {} impl ChainProvider for CardanoClient { diff --git a/crates/gem_cosmos/src/rpc/client.rs b/crates/gem_cosmos/src/rpc/client.rs index 157bdb0d2..4adec8b7c 100644 --- a/crates/gem_cosmos/src/rpc/client.rs +++ b/crates/gem_cosmos/src/rpc/client.rs @@ -7,7 +7,7 @@ use crate::models::{ AnnualProvisionsResponse, BlockResponse, InflationResponse, OsmosisEpochProvisionsResponse, OsmosisMintParamsResponse, StakingPoolResponse, SupplyResponse, TransactionResponse, TransactionsResponse, ValidatorsResponse, }; -use chain_traits::{ChainAccount, ChainAddressStatus, ChainPerpetual, ChainTraits}; +use chain_traits::{ChainAccount, ChainAddressStatus, ChainPerpetual, ChainSimulation, ChainTraits}; use gem_client::{Client, ClientExt}; use primitives::chain_cosmos::CosmosChain; @@ -158,6 +158,8 @@ impl ChainPerpetual for CosmosClient {} impl ChainAddressStatus for CosmosClient {} +impl ChainSimulation for CosmosClient {} + impl ChainTraits for CosmosClient {} impl chain_traits::ChainProvider for CosmosClient { diff --git a/crates/gem_evm/Cargo.toml b/crates/gem_evm/Cargo.toml index 4e013abd9..eda04bb17 100644 --- a/crates/gem_evm/Cargo.toml +++ b/crates/gem_evm/Cargo.toml @@ -5,7 +5,7 @@ edition = { workspace = true } [features] default = [] -rpc = ["gem_jsonrpc/client", "dep:async-trait", "dep:chain_traits"] +rpc = ["gem_jsonrpc/client", "dep:async-trait", "dep:chain_traits", "dep:futures"] reqwest = ["gem_jsonrpc/reqwest", "gem_client/reqwest", "dep:reqwest"] signer = ["dep:alloy-signer", "dep:alloy-signer-local", "dep:alloy-network", "dep:alloy-rlp", "dep:alloy-consensus"] chain_integration_tests = ["rpc", "reqwest", "settings/testkit"] @@ -43,6 +43,7 @@ reqwest = { workspace = true, features = ["json"], optional = true } chrono = { workspace = true } async-trait = { workspace = true, optional = true } chain_traits = { path = "../chain_traits", optional = true } +futures = { workspace = true, optional = true } [dev-dependencies] primitives = { path = "../primitives", features = ["testkit"] } diff --git a/crates/gem_evm/src/provider/mod.rs b/crates/gem_evm/src/provider/mod.rs index c983eaccd..786c4eca4 100644 --- a/crates/gem_evm/src/provider/mod.rs +++ b/crates/gem_evm/src/provider/mod.rs @@ -5,6 +5,7 @@ pub mod balances_smartchain; pub mod preload; pub mod preload_mapper; pub mod preload_optimism; +pub mod simulation; pub mod staking; pub mod staking_ethereum; pub mod staking_mapper; diff --git a/crates/gem_evm/src/provider/simulation.rs b/crates/gem_evm/src/provider/simulation.rs new file mode 100644 index 000000000..5e8d48a0e --- /dev/null +++ b/crates/gem_evm/src/provider/simulation.rs @@ -0,0 +1,224 @@ +use alloy_primitives::{Address, hex}; +use async_trait::async_trait; +use chain_primitives::{BalanceDiff, BalanceDiffMap}; +use chain_traits::ChainSimulation; +use gem_client::Client; +use num_bigint::BigInt; +use num_traits::Num; +use primitives::{AssetId, Chain, SimulationInput, SimulationResult}; +use std::collections::{HashMap, HashSet}; + +use crate::ethereum_address_checksum; +use crate::jsonrpc::TransactionObject; +use crate::rpc::client::EthereumClient; +use crate::rpc::debug_trace::{CallFrame, CallLog, PrestateDiffResult}; +use crate::rpc::mapper::TRANSFER_TOPIC; + +#[async_trait] +impl ChainSimulation for EthereumClient { + async fn simulate_transaction(&self, input: SimulationInput) -> Result> { + let tx: TransactionObject = serde_json::from_str(&input.encoded_transaction)?; + + let (prestate, call_frame) = futures::try_join!(self.debug_trace_call_prestate(&tx), self.debug_trace_call_logs(&tx),)?; + + let success = call_frame.error.is_none(); + let error = call_frame.error.clone().or_else(|| call_frame.revert_reason.clone()); + let gas_used = call_frame.gas_used.as_deref().and_then(|g| u64::from_str_radix(g.trim_start_matches("0x"), 16).ok()); + + let balance_changes = map_balance_changes(self.get_chain(), &prestate, &call_frame); + + Ok(SimulationResult { + success, + error, + logs: vec![], + units_consumed: gas_used, + balance_changes, + }) + } +} + +fn map_balance_changes(chain: Chain, prestate: &PrestateDiffResult, call_frame: &CallFrame) -> BalanceDiffMap { + let mut map: BalanceDiffMap = HashMap::new(); + + // Native balance diffs from prestateTracer + let all_addresses: HashSet<&String> = prestate.pre.keys().chain(prestate.post.keys()).collect(); + + for address in all_addresses { + let pre_balance = prestate.pre.get(address).and_then(|s| s.balance.as_deref()).and_then(parse_hex_bigint); + let post_balance = prestate.post.get(address).and_then(|s| s.balance.as_deref()).and_then(parse_hex_bigint); + + let (from_value, to_value, diff) = match (pre_balance, post_balance) { + (Some(pre), Some(post)) => { + let d = &post - ⪯ + if d == BigInt::from(0) { + continue; + } + (Some(pre), Some(post), d) + } + (Some(pre), None) => { + let d = -pre.clone(); + (Some(pre), Some(BigInt::from(0)), d) + } + (None, Some(post)) => { + let d = post.clone(); + (Some(BigInt::from(0)), Some(post), d) + } + (None, None) => continue, + }; + + let checksum = ethereum_address_checksum(address).unwrap_or_default(); + map.entry(checksum).or_default().push(BalanceDiff { + asset_id: AssetId { chain, token_id: None }, + from_value: Some(from_value.unwrap_or_default()), + to_value: Some(to_value.unwrap_or_default()), + diff, + }); + } + + // ERC20 token transfers from callTracer logs + let mut token_transfers: HashMap> = HashMap::new(); + + for log in &call_frame.logs { + if let Some((from, to, value)) = parse_transfer_log(log) { + let token_address = ethereum_address_checksum(&log.address).unwrap_or_default(); + *token_transfers.entry(from).or_default().entry(token_address.clone()).or_default() -= value.clone(); + *token_transfers.entry(to).or_default().entry(token_address).or_default() += value; + } + } + + for (address, tokens) in token_transfers { + for (token_address, net_diff) in tokens { + if net_diff != BigInt::from(0) { + map.entry(address.clone()).or_default().push(BalanceDiff { + asset_id: AssetId { + chain, + token_id: Some(token_address), + }, + from_value: None, + to_value: None, + diff: net_diff, + }); + } + } + } + + map +} + +fn parse_transfer_log(log: &CallLog) -> Option<(String, String, BigInt)> { + if log.topics.len() < 3 || log.topics[0] != TRANSFER_TOPIC { + return None; + } + + let from_bytes = hex::decode(log.topics[1].trim_start_matches("0x")).ok()?; + let to_bytes = hex::decode(log.topics[2].trim_start_matches("0x")).ok()?; + + if from_bytes.len() != 32 || to_bytes.len() != 32 { + return None; + } + + let from = Address::from_slice(&from_bytes[12..]).to_checksum(None); + let to = Address::from_slice(&to_bytes[12..]).to_checksum(None); + + let data = log.data.trim_start_matches("0x"); + let value = BigInt::from_str_radix(data, 16).ok()?; + + Some((from, to, value)) +} + +fn parse_hex_bigint(hex_str: &str) -> Option { + let stripped = hex_str.trim_start_matches("0x"); + if stripped.is_empty() { + return Some(BigInt::from(0)); + } + BigInt::from_str_radix(stripped, 16).ok() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::rpc::debug_trace::AccountState; + + #[test] + fn test_native_balance_changes() { + let mut pre = HashMap::new(); + pre.insert( + "0x52a07c930157d07d9effd147ecf41c5cbbc6000c".to_string(), + AccountState { + balance: Some("0x28268111de83a9d".to_string()), + }, + ); + let mut post = HashMap::new(); + post.insert( + "0x52a07c930157d07d9effd147ecf41c5cbbc6000c".to_string(), + AccountState { + balance: Some("0x4bd382b322e4810".to_string()), + }, + ); + + let prestate = PrestateDiffResult { pre, post }; + let call_frame = CallFrame { + gas_used: None, + output: None, + error: None, + revert_reason: None, + logs: vec![], + }; + + let result = map_balance_changes(Chain::Ethereum, &prestate, &call_frame); + + let sender = "0x52A07c930157d07D9EffD147ecF41C5cBbC6000c"; + let diffs = result.get(sender).expect("sender diffs"); + assert_eq!(diffs.len(), 1); + assert_eq!(diffs[0].asset_id, AssetId::from_chain(Chain::Ethereum)); + assert!(diffs[0].diff > BigInt::from(0)); + } + + #[test] + fn test_erc20_transfer_from_logs() { + let prestate = PrestateDiffResult::default(); + let call_frame = CallFrame { + gas_used: None, + output: None, + error: None, + revert_reason: None, + logs: vec![CallLog { + address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".to_string(), + topics: vec![ + TRANSFER_TOPIC.to_string(), + "0x00000000000000000000000052a07c930157d07d9effd147ecf41c5cbbc6000c".to_string(), + "0x000000000000000000000000def1c0ded9bec7f1a1670819833240f027b25eff".to_string(), + ], + data: "0x000000000000000000000000000000000000000000000000000000003b9aca00".to_string(), + }], + }; + + let result = map_balance_changes(Chain::Ethereum, &prestate, &call_frame); + + let sender = "0x52A07c930157d07D9EffD147ecF41C5cBbC6000c"; + let sender_diffs = result.get(sender).expect("sender diffs"); + assert_eq!(sender_diffs.len(), 1); + assert!(sender_diffs[0].diff < BigInt::from(0)); + assert_eq!(sender_diffs[0].asset_id.token_id.as_deref(), Some("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")); + + let receiver = "0xDef1C0ded9bec7F1a1670819833240f027b25EfF"; + let receiver_diffs = result.get(receiver).expect("receiver diffs"); + assert_eq!(receiver_diffs.len(), 1); + assert!(receiver_diffs[0].diff > BigInt::from(0)); + } + + #[test] + fn test_no_changes_produces_empty_map() { + let prestate = PrestateDiffResult::default(); + let call_frame = CallFrame { + gas_used: None, + output: None, + error: None, + revert_reason: None, + logs: vec![], + }; + + let result = map_balance_changes(Chain::Ethereum, &prestate, &call_frame); + assert!(result.is_empty()); + } +} diff --git a/crates/gem_evm/src/rpc/client.rs b/crates/gem_evm/src/rpc/client.rs index 4051a1884..48759a739 100644 --- a/crates/gem_evm/src/rpc/client.rs +++ b/crates/gem_evm/src/rpc/client.rs @@ -12,8 +12,10 @@ use std::str::FromStr; use super::{ ankr::AnkrClient, + debug_trace::{CallFrame, PrestateDiffResult}, model::{Block, BlockTransactionsIds, EthSyncingStatus, Log, Transaction, TransactionReciept, TransactionReplayTrace}, }; +use crate::jsonrpc::TransactionObject; use crate::models::fee::EthereumFeeHistory; #[cfg(feature = "rpc")] use crate::multicall3::{ @@ -282,4 +284,20 @@ impl EthereumClient { Ok(multicall_results) } + + pub async fn debug_trace_call_prestate(&self, tx: &TransactionObject) -> Result { + let params = json!([tx, "latest", { + "tracer": "prestateTracer", + "tracerConfig": { "diffMode": true } + }]); + self.client.call("debug_traceCall", params).await + } + + pub async fn debug_trace_call_logs(&self, tx: &TransactionObject) -> Result { + let params = json!([tx, "latest", { + "tracer": "callTracer", + "tracerConfig": { "onlyTopCall": true, "withLog": true } + }]); + self.client.call("debug_traceCall", params).await + } } diff --git a/crates/gem_evm/src/rpc/debug_trace.rs b/crates/gem_evm/src/rpc/debug_trace.rs new file mode 100644 index 000000000..8416a3a2a --- /dev/null +++ b/crates/gem_evm/src/rpc/debug_trace.rs @@ -0,0 +1,31 @@ +use serde::Deserialize; +use std::collections::HashMap; + +#[derive(Debug, Clone, Default, Deserialize)] +pub struct PrestateDiffResult { + pub pre: HashMap, + pub post: HashMap, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct AccountState { + pub balance: Option, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CallFrame { + pub gas_used: Option, + pub output: Option, + pub error: Option, + pub revert_reason: Option, + #[serde(default)] + pub logs: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct CallLog { + pub address: String, + pub topics: Vec, + pub data: String, +} diff --git a/crates/gem_evm/src/rpc/mod.rs b/crates/gem_evm/src/rpc/mod.rs index a911bdad7..075ef6d23 100644 --- a/crates/gem_evm/src/rpc/mod.rs +++ b/crates/gem_evm/src/rpc/mod.rs @@ -1,6 +1,7 @@ pub mod ankr; pub mod balance_differ; pub mod client; +pub mod debug_trace; pub mod mapper; pub mod model; pub mod staking_mapper; diff --git a/crates/gem_hypercore/src/provider/mod.rs b/crates/gem_hypercore/src/provider/mod.rs index 44f203b07..289a5dd46 100644 --- a/crates/gem_hypercore/src/provider/mod.rs +++ b/crates/gem_hypercore/src/provider/mod.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use chain_traits::ChainAccount; +use chain_traits::{ChainAccount, ChainSimulation}; use gem_client::Client; pub mod balances; @@ -24,3 +24,6 @@ use crate::rpc::client::HyperCoreClient; #[async_trait] impl ChainAccount for HyperCoreClient {} + +#[async_trait] +impl ChainSimulation for HyperCoreClient {} diff --git a/crates/gem_near/src/rpc/client.rs b/crates/gem_near/src/rpc/client.rs index 3915c0622..97bc15e69 100644 --- a/crates/gem_near/src/rpc/client.rs +++ b/crates/gem_near/src/rpc/client.rs @@ -1,5 +1,5 @@ use crate::models::{Account, AccountAccessKey, Block, BroadcastResult, GasPrice, GenesisConfig}; -use chain_traits::{ChainAccount, ChainAddressStatus, ChainPerpetual, ChainProvider, ChainStaking, ChainToken, ChainTraits}; +use chain_traits::{ChainAccount, ChainAddressStatus, ChainPerpetual, ChainProvider, ChainSimulation, ChainStaking, ChainToken, ChainTraits}; use gem_client::Client; use gem_jsonrpc::{client::JsonRpcClient, types::JsonRpcError}; use primitives::{Asset, Chain}; @@ -83,4 +83,5 @@ impl ChainPerpetual for NearClient {} impl ChainAddressStatus for NearClient {} impl ChainAccount for NearClient {} impl ChainToken for NearClient {} +impl ChainSimulation for NearClient {} impl ChainTraits for NearClient {} diff --git a/crates/gem_polkadot/src/rpc/client.rs b/crates/gem_polkadot/src/rpc/client.rs index fc5351449..7a6f7ade1 100644 --- a/crates/gem_polkadot/src/rpc/client.rs +++ b/crates/gem_polkadot/src/rpc/client.rs @@ -1,6 +1,6 @@ use std::error::Error; -use chain_traits::{ChainAccount, ChainAddressStatus, ChainPerpetual, ChainProvider, ChainTraits}; +use chain_traits::{ChainAccount, ChainAddressStatus, ChainPerpetual, ChainProvider, ChainSimulation, ChainTraits}; use gem_client::{Client, ClientExt}; use primitives::{Asset, Chain}; @@ -72,3 +72,4 @@ impl ChainTraits for PolkadotClient {} impl ChainAccount for PolkadotClient {} impl ChainPerpetual for PolkadotClient {} impl ChainAddressStatus for PolkadotClient {} +impl ChainSimulation for PolkadotClient {} diff --git a/crates/gem_solana/Cargo.toml b/crates/gem_solana/Cargo.toml index 7e62ba08b..3432fd561 100644 --- a/crates/gem_solana/Cargo.toml +++ b/crates/gem_solana/Cargo.toml @@ -11,6 +11,7 @@ rpc = [ "dep:gem_client", "dep:chain_traits", "dep:futures", + "dep:solana-primitives", ] signer = ["dep:signer", "dep:solana-primitives", "dep:num-traits"] reqwest = ["gem_jsonrpc/reqwest"] diff --git a/crates/gem_solana/src/models/mod.rs b/crates/gem_solana/src/models/mod.rs index 10cd64412..a63b419b1 100644 --- a/crates/gem_solana/src/models/mod.rs +++ b/crates/gem_solana/src/models/mod.rs @@ -4,6 +4,7 @@ pub mod blockhash; pub mod jito; pub mod prioritization_fee; pub mod rpc; +pub mod simulation; pub mod stake; pub mod token; pub mod token_account; diff --git a/crates/gem_solana/src/models/simulation.rs b/crates/gem_solana/src/models/simulation.rs new file mode 100644 index 000000000..bdb61ac61 --- /dev/null +++ b/crates/gem_solana/src/models/simulation.rs @@ -0,0 +1,14 @@ +use crate::models::token::TokenBalance; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SimulateTransactionValue { + pub err: Option, + pub logs: Option>, + pub units_consumed: Option, + pub pre_balances: Vec, + pub post_balances: Vec, + pub pre_token_balances: Option>, + pub post_token_balances: Option>, +} diff --git a/crates/gem_solana/src/provider/mod.rs b/crates/gem_solana/src/provider/mod.rs index e793fb042..0c1baafd5 100644 --- a/crates/gem_solana/src/provider/mod.rs +++ b/crates/gem_solana/src/provider/mod.rs @@ -6,6 +6,8 @@ pub mod testkit; #[cfg(feature = "rpc")] pub mod preload_mapper; #[cfg(feature = "rpc")] +pub mod simulation; +#[cfg(feature = "rpc")] pub mod staking; #[cfg(feature = "rpc")] pub mod staking_mapper; diff --git a/crates/gem_solana/src/provider/simulation.rs b/crates/gem_solana/src/provider/simulation.rs new file mode 100644 index 000000000..f017ac6eb --- /dev/null +++ b/crates/gem_solana/src/provider/simulation.rs @@ -0,0 +1,198 @@ +use std::collections::{HashMap, HashSet}; +use std::error::Error; + +use async_trait::async_trait; +use base64::{Engine, engine::general_purpose::STANDARD}; +use chain_traits::ChainSimulation; +use gem_client::Client; +use num_bigint::BigInt; +use primitives::{AssetId, BalanceDiff, BalanceDiffMap, Chain, SimulationInput, SimulationResult}; +use solana_primitives::VersionedTransaction; + +use crate::models::token::TokenBalance; +use crate::rpc::client::SolanaClient; + +#[async_trait] +impl ChainSimulation for SolanaClient { + async fn simulate_transaction(&self, input: SimulationInput) -> Result> { + let tx_bytes = STANDARD.decode(&input.encoded_transaction).map_err(|e| format!("invalid base64: {e}"))?; + + let transaction = VersionedTransaction::deserialize_with_version(&tx_bytes).map_err(|e| format!("parse transaction: {e}"))?; + + let account_keys: Vec = transaction.account_keys().iter().map(|k| k.to_string()).collect(); + + let sim_result = self.simulate_transaction(&input.encoded_transaction).await?; + + if let Some(err) = &sim_result.err { + return Err(err.to_string().into()); + } + + let balance_changes = map_balance_changes( + &account_keys, + &sim_result.pre_balances, + &sim_result.post_balances, + &sim_result.pre_token_balances.unwrap_or_default(), + &sim_result.post_token_balances.unwrap_or_default(), + ); + + Ok(SimulationResult { + success: true, + error: None, + logs: sim_result.logs.unwrap_or_default(), + units_consumed: sim_result.units_consumed, + balance_changes, + }) + } +} + +fn map_balance_changes( + account_keys: &[String], + pre_balances: &[u64], + post_balances: &[u64], + pre_token_balances: &[TokenBalance], + post_token_balances: &[TokenBalance], +) -> BalanceDiffMap { + let mut result: BalanceDiffMap = HashMap::new(); + + for (i, address) in account_keys.iter().enumerate() { + let pre = pre_balances.get(i).copied().unwrap_or(0); + let post = post_balances.get(i).copied().unwrap_or(0); + if pre != post { + result.entry(address.clone()).or_default().push(BalanceDiff { + asset_id: AssetId::from_chain(Chain::Solana), + from_value: Some(BigInt::from(pre)), + to_value: Some(BigInt::from(post)), + diff: BigInt::from(post as i128 - pre as i128), + }); + } + } + + let owners: HashSet<&str> = pre_token_balances.iter().chain(post_token_balances.iter()).map(|tb| tb.owner.as_str()).collect(); + + for owner in owners { + let pre_by_mint: HashMap<&str, &TokenBalance> = pre_token_balances.iter().filter(|tb| tb.owner == owner).map(|tb| (tb.mint.as_str(), tb)).collect(); + + let post_by_mint: HashMap<&str, &TokenBalance> = post_token_balances.iter().filter(|tb| tb.owner == owner).map(|tb| (tb.mint.as_str(), tb)).collect(); + + let all_mints: HashSet<&str> = pre_by_mint.keys().chain(post_by_mint.keys()).copied().collect(); + + for mint in all_mints { + let pre_amount = pre_by_mint.get(mint).map(|tb| BigInt::from(tb.get_amount())).unwrap_or_default(); + let post_amount = post_by_mint.get(mint).map(|tb| BigInt::from(tb.get_amount())).unwrap_or_default(); + let diff = &post_amount - &pre_amount; + + if diff != BigInt::from(0) { + result.entry(owner.to_string()).or_default().push(BalanceDiff { + asset_id: AssetId::from_token(Chain::Solana, mint), + from_value: Some(pre_amount), + to_value: Some(post_amount), + diff, + }); + } + } + } + + result +} + +#[cfg(test)] +mod tests { + use super::*; + use num_bigint::BigUint; + + use crate::models::token::TokenAmount; + + fn make_token_balance(account_index: i64, mint: &str, owner: &str, amount: u64) -> TokenBalance { + TokenBalance::new(account_index, mint.to_string(), owner.to_string(), TokenAmount { amount: BigUint::from(amount) }) + } + + #[test] + fn test_native_sol_balance_change() { + let keys = vec!["addr1".to_string()]; + let pre = vec![1_000_000_000u64]; + let post = vec![900_000_000u64]; + + let changes = map_balance_changes(&keys, &pre, &post, &[], &[]); + + let diffs = changes.get("addr1").unwrap(); + assert_eq!(diffs.len(), 1); + assert_eq!(diffs[0].asset_id, AssetId::from_chain(Chain::Solana)); + assert_eq!(diffs[0].from_value, Some(BigInt::from(1_000_000_000))); + assert_eq!(diffs[0].to_value, Some(BigInt::from(900_000_000))); + assert_eq!(diffs[0].diff, BigInt::from(-100_000_000)); + } + + #[test] + fn test_no_change() { + let keys = vec!["addr1".to_string()]; + let pre = vec![1_000_000_000u64]; + let post = vec![1_000_000_000u64]; + + let changes = map_balance_changes(&keys, &pre, &post, &[], &[]); + assert!(changes.is_empty()); + } + + #[test] + fn test_spl_token_balance_change() { + let mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; + let owner = "wallet_owner"; + let keys = vec!["token_account_addr".to_string()]; + let pre_balances = vec![2_039_280u64]; + let post_balances = vec![2_039_280u64]; + let pre_tokens = vec![make_token_balance(0, mint, owner, 1_000_000)]; + let post_tokens = vec![make_token_balance(0, mint, owner, 500_000)]; + + let changes = map_balance_changes(&keys, &pre_balances, &post_balances, &pre_tokens, &post_tokens); + + let diffs = changes.get(owner).unwrap(); + assert_eq!(diffs.len(), 1); + assert_eq!(diffs[0].asset_id, AssetId::from_token(Chain::Solana, mint)); + assert_eq!(diffs[0].diff, BigInt::from(-500_000)); + } + + #[test] + fn test_new_token_account_created() { + let mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; + let owner = "wallet_owner"; + let keys = vec!["new_token_addr".to_string()]; + let pre_balances = vec![0u64]; + let post_balances = vec![2_039_280u64]; + let pre_tokens: Vec = vec![]; + let post_tokens = vec![make_token_balance(0, mint, owner, 1_000_000)]; + + let changes = map_balance_changes(&keys, &pre_balances, &post_balances, &pre_tokens, &post_tokens); + + let sol_diffs = changes.get("new_token_addr").unwrap(); + assert_eq!(sol_diffs.len(), 1); + assert_eq!(sol_diffs[0].asset_id, AssetId::from_chain(Chain::Solana)); + + let token_diffs = changes.get(owner).unwrap(); + assert_eq!(token_diffs.len(), 1); + assert_eq!(token_diffs[0].asset_id, AssetId::from_token(Chain::Solana, mint)); + assert_eq!(token_diffs[0].from_value, Some(BigInt::from(0))); + assert_eq!(token_diffs[0].to_value, Some(BigInt::from(1_000_000))); + assert_eq!(token_diffs[0].diff, BigInt::from(1_000_000)); + } + + #[test] + fn test_multiple_accounts_mixed() { + let mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; + let keys = vec!["sol_addr".to_string(), "token_addr".to_string()]; + let pre_balances = vec![1_000_000_000u64, 2_039_280]; + let post_balances = vec![900_000_000u64, 2_039_280]; + let pre_tokens = vec![make_token_balance(1, mint, "wallet_owner", 1_000_000)]; + let post_tokens = vec![make_token_balance(1, mint, "wallet_owner", 2_000_000)]; + + let changes = map_balance_changes(&keys, &pre_balances, &post_balances, &pre_tokens, &post_tokens); + + let sol_diffs = changes.get("sol_addr").unwrap(); + assert_eq!(sol_diffs.len(), 1); + assert_eq!(sol_diffs[0].asset_id, AssetId::from_chain(Chain::Solana)); + assert_eq!(sol_diffs[0].diff, BigInt::from(-100_000_000)); + + let token_diffs = changes.get("wallet_owner").unwrap(); + assert_eq!(token_diffs.len(), 1); + assert_eq!(token_diffs[0].asset_id, AssetId::from_token(Chain::Solana, mint)); + assert_eq!(token_diffs[0].diff, BigInt::from(1_000_000)); + } +} diff --git a/crates/gem_solana/src/rpc/client.rs b/crates/gem_solana/src/rpc/client.rs index daebe2e98..ff9c1e6ee 100644 --- a/crates/gem_solana/src/rpc/client.rs +++ b/crates/gem_solana/src/rpc/client.rs @@ -4,6 +4,7 @@ use crate::models::{ balances::SolanaBalance, blockhash::SolanaBlockhashResult, prioritization_fee::SolanaPrioritizationFee, + simulation::SimulateTransactionValue, transaction::{BlockTransactions, SolanaTransaction}, }; use chain_traits::ChainProvider; @@ -253,6 +254,18 @@ impl SolanaClient { Ok(transactions) } + pub async fn simulate_transaction(&self, encoded_tx: &str) -> Result { + let params = serde_json::json!([ + encoded_tx, + { + "encoding": "base64", + "sigVerify": false, + "replaceRecentBlockhash": true + } + ]); + self.rpc_call("simulateTransaction", params).await + } + pub async fn get_token_accounts(&self, address: &str, token_mints: &[String]) -> Result>>, Box> { let calls: Vec<(String, serde_json::Value)> = token_mints .iter() diff --git a/crates/gem_stellar/src/rpc/client.rs b/crates/gem_stellar/src/rpc/client.rs index a38a11ea5..aeb2c12d1 100644 --- a/crates/gem_stellar/src/rpc/client.rs +++ b/crates/gem_stellar/src/rpc/client.rs @@ -7,7 +7,7 @@ use crate::models::node::NodeStatus; use crate::models::transaction::{Payment, StellarTransactionBroadcast, StellarTransactionStatus}; use crate::models::{AccountEmpty, AccountResult}; -use chain_traits::{ChainAddressStatus, ChainPerpetual, ChainProvider, ChainStaking, ChainTraits}; +use chain_traits::{ChainAddressStatus, ChainPerpetual, ChainProvider, ChainSimulation, ChainStaking, ChainTraits}; use gem_client::{Client, ClientError, ClientExt, ContentType}; use primitives::Chain; use std::collections::HashMap; @@ -113,6 +113,8 @@ impl ChainAddressStatus for StellarClient {} impl chain_traits::ChainAccount for StellarClient {} +impl ChainSimulation for StellarClient {} + impl ChainTraits for StellarClient {} impl ChainProvider for StellarClient { diff --git a/crates/gem_sui/src/gas_budget.rs b/crates/gem_sui/src/gas_budget.rs index 6ebba8268..5706360f9 100644 --- a/crates/gem_sui/src/gas_budget.rs +++ b/crates/gem_sui/src/gas_budget.rs @@ -1,12 +1,20 @@ -use crate::models::InspectGasUsed; +use crate::models::GasUsed; +use num_bigint::BigUint; +use num_traits::ToPrimitive; use std::cmp::max; -pub struct GasBudgetCalculator {} +pub struct GasBudgetCalculator; impl GasBudgetCalculator { - pub fn gas_budget(gas_used: &InspectGasUsed) -> u64 { - let computation_budget = gas_used.computation_cost; - let budget = max(computation_budget, computation_budget + gas_used.storage_cost - gas_used.storage_rebate); - budget * 120 / 100 + pub fn total_gas(gas_used: &GasUsed) -> u64 { + let computation = &gas_used.computation_cost; + let storage = &gas_used.storage_cost; + let rebate = &gas_used.storage_rebate; + let net = if computation + storage > *rebate { + computation + storage - rebate + } else { + BigUint::from(0u64) + }; + max(computation.clone(), net).to_u64().unwrap_or(0) } } diff --git a/crates/gem_sui/src/models/mod.rs b/crates/gem_sui/src/models/mod.rs index e343a568d..cbc169c9b 100644 --- a/crates/gem_sui/src/models/mod.rs +++ b/crates/gem_sui/src/models/mod.rs @@ -25,4 +25,4 @@ pub use staking::RpcSuiSystemState; #[cfg(feature = "rpc")] pub use staking::{EventStake, EventUnstake, ValidatorApy, ValidatorInfo, ValidatorSet}; #[cfg(feature = "rpc")] -pub use transaction::{Digest, Digests, Effect, Event, GasUsed, ResultData, Status, TransactionBroadcast}; +pub use transaction::{Digest, Digests, DryRunResult, Effect, Event, GasUsed, ResultData, Status, TransactionBroadcast}; diff --git a/crates/gem_sui/src/models/transaction.rs b/crates/gem_sui/src/models/transaction.rs index 5e7ec21c0..b33ed8465 100644 --- a/crates/gem_sui/src/models/transaction.rs +++ b/crates/gem_sui/src/models/transaction.rs @@ -17,6 +17,14 @@ pub struct SuiTransaction { pub effects: SuiEffects, } +#[cfg(feature = "rpc")] +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DryRunResult { + pub effects: SuiEffects, + pub balance_changes: Vec, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SuiStatus { @@ -31,6 +39,16 @@ pub struct SuiEffects { pub created: Option>, } +impl SuiEffects { + pub fn is_success(&self) -> bool { + self.status.status == "success" + } + + pub fn error(&self) -> Option { + if self.is_success() { None } else { Some(self.status.status.clone()) } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SuiObjectChange { diff --git a/crates/gem_sui/src/provider/mod.rs b/crates/gem_sui/src/provider/mod.rs index 7f91cbe22..1fba4fdac 100644 --- a/crates/gem_sui/src/provider/mod.rs +++ b/crates/gem_sui/src/provider/mod.rs @@ -3,6 +3,8 @@ pub mod balances; pub mod balances_mapper; pub mod preload; pub mod preload_mapper; +#[cfg(feature = "rpc")] +pub mod simulation; pub mod staking; pub mod staking_mapper; pub mod state; diff --git a/crates/gem_sui/src/provider/simulation.rs b/crates/gem_sui/src/provider/simulation.rs new file mode 100644 index 000000000..a96164195 --- /dev/null +++ b/crates/gem_sui/src/provider/simulation.rs @@ -0,0 +1,52 @@ +use std::collections::HashMap; + +use async_trait::async_trait; +use chain_traits::ChainSimulation; +use gem_client::Client; +use primitives::{BalanceDiff, BalanceDiffMap, SimulationInput, SimulationResult}; + +use crate::gas_budget::GasBudgetCalculator; +use crate::models::coin::BalanceChange; +use crate::provider::transactions_mapper::map_asset_id; +use crate::rpc::client::SuiClient; + +fn map_dry_run_balance_changes(balance_changes: &[BalanceChange]) -> BalanceDiffMap { + let mut result: BalanceDiffMap = HashMap::new(); + + for change in balance_changes { + let owner = match change.owner.get_address_owner() { + Some(addr) => addr, + None => continue, + }; + let asset_id = map_asset_id(&change.coin_type); + result.entry(owner).or_default().push(BalanceDiff { + asset_id, + from_value: None, + to_value: None, + diff: change.amount.clone(), + }); + } + + result +} + +#[async_trait] +impl ChainSimulation for SuiClient { + async fn simulate_transaction(&self, input: SimulationInput) -> Result> { + let result = self.dry_run(input.encoded_transaction).await?; + let success = result.effects.is_success(); + let error = result.effects.error(); + + let units_consumed = Some(GasBudgetCalculator::total_gas(&result.effects.gas_used)); + + let balance_changes = map_dry_run_balance_changes(&result.balance_changes); + + Ok(SimulationResult { + success, + error, + logs: vec![], + units_consumed, + balance_changes, + }) + } +} diff --git a/crates/gem_sui/src/rpc/client.rs b/crates/gem_sui/src/rpc/client.rs index cdf389aa8..d07a173df 100644 --- a/crates/gem_sui/src/rpc/client.rs +++ b/crates/gem_sui/src/rpc/client.rs @@ -17,11 +17,11 @@ use serde::de::DeserializeOwned; use sui_types::Address; use crate::models::staking::{SuiStakeDelegation, SuiSystemState, SuiValidators}; -use crate::models::transaction::{SuiBroadcastTransaction, SuiTransaction}; +use crate::models::transaction::SuiBroadcastTransaction; use crate::models::{Balance, Checkpoint, Digest, Digests, ResultData, TransactionBlocks}; #[cfg(feature = "rpc")] use crate::models::{CoinAsset, InspectResult, SuiObject}; -use crate::models::{SuiCoin, SuiCoinMetadata}; +use crate::models::{DryRunResult, SuiCoin, SuiCoinMetadata}; #[cfg(feature = "rpc")] use crate::{ SUI_COIN_TYPE, SUI_COIN_TYPE_FULL, @@ -123,7 +123,7 @@ impl SuiClient { Ok(self.client.call::>("sui_getObject", params).await?.data) } - pub async fn dry_run(&self, tx_data: String) -> Result> { + pub async fn dry_run(&self, tx_data: String) -> Result> { let params = serde_json::json!([tx_data]); Ok(self.client.call("sui_dryRunTransactionBlock", params).await?) } diff --git a/crates/gem_ton/src/rpc/client.rs b/crates/gem_ton/src/rpc/client.rs index ded4750c5..3460d7598 100644 --- a/crates/gem_ton/src/rpc/client.rs +++ b/crates/gem_ton/src/rpc/client.rs @@ -3,7 +3,7 @@ use std::error::Error; use primitives::{Asset, AssetId, AssetType, chain::Chain}; use serde_json; -use chain_traits::{ChainAccount, ChainAddressStatus, ChainPerpetual, ChainStaking, ChainTraits}; +use chain_traits::{ChainAccount, ChainAddressStatus, ChainPerpetual, ChainSimulation, ChainStaking, ChainTraits}; use gem_client::{Client, ClientExt}; use crate::models::{ApiResult, BroadcastTransaction, Chainhead, JettonInfo, JettonWalletsResponse, MessageTransactions, SimpleJettonBalance, WalletInfo}; @@ -85,6 +85,7 @@ impl ChainAccount for TonClient {} impl ChainPerpetual for TonClient {} impl ChainAddressStatus for TonClient {} impl ChainStaking for TonClient {} +impl ChainSimulation for TonClient {} impl chain_traits::ChainProvider for TonClient { fn get_chain(&self) -> primitives::Chain { Chain::Ton diff --git a/crates/gem_tron/src/rpc/client.rs b/crates/gem_tron/src/rpc/client.rs index 7ec751f06..f2a28316d 100644 --- a/crates/gem_tron/src/rpc/client.rs +++ b/crates/gem_tron/src/rpc/client.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use chain_traits::{ChainAccount, ChainPerpetual, ChainTraits}; +use chain_traits::{ChainAccount, ChainPerpetual, ChainSimulation, ChainTraits}; use num_bigint::BigUint; use primitives::{Asset, AssetId, asset_type::AssetType, chain::Chain}; use std::{error::Error, str::FromStr}; @@ -262,6 +262,9 @@ impl ChainAccount for TronClient {} #[async_trait] impl ChainPerpetual for TronClient {} +#[async_trait] +impl ChainSimulation for TronClient {} + impl chain_traits::ChainProvider for TronClient { fn get_chain(&self) -> primitives::Chain { Chain::Tron diff --git a/crates/gem_xrp/src/rpc/client.rs b/crates/gem_xrp/src/rpc/client.rs index 2c4acb9c4..3f9eb2c24 100644 --- a/crates/gem_xrp/src/rpc/client.rs +++ b/crates/gem_xrp/src/rpc/client.rs @@ -3,7 +3,7 @@ use std::error::Error; use crate::models::rpc::{AccountInfo, AccountInfoResult, AccountLedger, AccountObjects, FeesResult, Ledger, LedgerCurrent, LedgerData, TransactionBroadcast, TransactionStatus}; -use chain_traits::{ChainAddressStatus, ChainPerpetual, ChainProvider, ChainStaking, ChainTraits}; +use chain_traits::{ChainAddressStatus, ChainPerpetual, ChainProvider, ChainSimulation, ChainStaking, ChainTraits}; use gem_client::Client; use gem_jsonrpc::client::JsonRpcClient as GenericJsonRpcClient; use primitives::Chain; @@ -126,6 +126,8 @@ impl ChainAddressStatus for XRPClient {} impl chain_traits::ChainAccount for XRPClient {} +impl ChainSimulation for XRPClient {} + impl ChainTraits for XRPClient {} impl ChainProvider for XRPClient { diff --git a/crates/primitives/src/balance_diff.rs b/crates/primitives/src/balance_diff.rs new file mode 100644 index 000000000..3e4e9a61e --- /dev/null +++ b/crates/primitives/src/balance_diff.rs @@ -0,0 +1,16 @@ +use std::collections::HashMap; + +use num_bigint::BigInt; + +use crate::AssetId; + +/// Address -> Vec +pub type BalanceDiffMap = HashMap>; + +#[derive(Debug, Clone)] +pub struct BalanceDiff { + pub asset_id: AssetId, + pub from_value: Option, + pub to_value: Option, + pub diff: BigInt, +} diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 3c04052b8..99cd272c6 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -264,6 +264,10 @@ pub mod ip_usage_type; pub use self::ip_usage_type::IpUsageType; pub mod metrics; pub use self::metrics::{ConsumerStatus, JobStatus, ParserStatus, ReportedError}; +pub mod balance_diff; +pub use self::balance_diff::{BalanceDiff, BalanceDiffMap}; +pub mod simulation; +pub use self::simulation::{SimulationInput, SimulationResult}; pub mod value_access; pub use self::value_access::ValueAccess; diff --git a/crates/primitives/src/simulation.rs b/crates/primitives/src/simulation.rs new file mode 100644 index 000000000..63d331c66 --- /dev/null +++ b/crates/primitives/src/simulation.rs @@ -0,0 +1,15 @@ +use crate::BalanceDiffMap; + +#[derive(Debug, Clone)] +pub struct SimulationInput { + pub encoded_transaction: String, +} + +#[derive(Debug, Clone)] +pub struct SimulationResult { + pub success: bool, + pub error: Option, + pub logs: Vec, + pub units_consumed: Option, + pub balance_changes: BalanceDiffMap, +} diff --git a/gemstone/src/gateway/mod.rs b/gemstone/src/gateway/mod.rs index f4a02e518..6b33fe5d1 100644 --- a/gemstone/src/gateway/mod.rs +++ b/gemstone/src/gateway/mod.rs @@ -291,6 +291,13 @@ impl GemGateway { self.with_provider(chain, |provider| async move { provider.get_perpetual_portfolio(address).await }).await } + pub async fn simulate_transaction(&self, chain: Chain, encoded_transaction: String) -> Result { + self.with_provider(chain, |provider| async move { + provider.simulate_transaction(primitives::SimulationInput { encoded_transaction }).await + }) + .await + } + pub async fn get_token_data(&self, chain: Chain, token_id: String) -> Result { self.with_provider(chain, |provider| async move { provider.get_token_data(token_id).await }).await } diff --git a/gemstone/src/models/mod.rs b/gemstone/src/models/mod.rs index 347d13a21..e8913519b 100644 --- a/gemstone/src/models/mod.rs +++ b/gemstone/src/models/mod.rs @@ -8,6 +8,7 @@ pub mod node; pub mod perpetual; pub mod portfolio; pub mod scan; +pub mod simulation; pub mod stake; pub mod swap; pub mod token; @@ -22,6 +23,7 @@ pub use node::*; pub use perpetual::*; pub use portfolio::*; pub use scan::*; +pub use simulation::*; pub use stake::*; pub use token::*; pub use transaction::*; diff --git a/gemstone/src/models/simulation.rs b/gemstone/src/models/simulation.rs new file mode 100644 index 000000000..66723f7c7 --- /dev/null +++ b/gemstone/src/models/simulation.rs @@ -0,0 +1,25 @@ +use std::collections::HashMap; + +use primitives::{AssetId, BalanceDiff, SimulationResult}; + +use super::custom_types::GemBigInt; + +pub type GemSimulationResult = SimulationResult; +pub type GemBalanceDiff = BalanceDiff; + +#[uniffi::remote(Record)] +pub struct SimulationResult { + pub success: bool, + pub error: Option, + pub logs: Vec, + pub units_consumed: Option, + pub balance_changes: HashMap>, +} + +#[uniffi::remote(Record)] +pub struct BalanceDiff { + pub asset_id: AssetId, + pub from_value: Option, + pub to_value: Option, + pub diff: GemBigInt, +}