diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index ac36b8426..b25cd6236 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -9,6 +9,8 @@ pub mod fuzz; pub mod meta; pub mod rainlang; pub mod remove_order; +#[cfg(not(target_family = "wasm"))] +pub mod replays; pub mod subgraph; pub mod transaction; pub mod types; diff --git a/crates/common/src/replays/mod.rs b/crates/common/src/replays/mod.rs new file mode 100644 index 000000000..be98db0df --- /dev/null +++ b/crates/common/src/replays/mod.rs @@ -0,0 +1,77 @@ +use alloy::primitives::B256; +use rain_interpreter_eval::{ + fork::{Forker, NewForkedEvm}, + trace::RainEvalResult, +}; +use url::Url; + +pub struct NewTradeReplayer { + pub fork_url: Url, +} +pub struct TradeReplayer { + forker: Forker, +} + +#[derive(Debug, thiserror::Error)] +pub enum TradeReplayerError { + #[error("Forker error: {0}")] + ForkerError(#[from] rain_interpreter_eval::error::ForkCallError), +} + +impl TradeReplayer { + pub async fn new(args: NewTradeReplayer) -> Result { + let forker = Forker::new_with_fork( + NewForkedEvm { + fork_url: args.fork_url.to_string(), + fork_block_number: None, + }, + None, + None, + ) + .await?; + + Ok(Self { forker }) + } + + pub async fn replay_tx(&mut self, tx_hash: B256) -> Result { + let res = self.forker.replay_transaction(tx_hash).await?; + Ok(res.into()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy::primitives::U256; + use rain_orderbook_env::CI_DEPLOY_POLYGON_RPC_URL; + use std::str::FromStr; + + #[tokio::test(flavor = "multi_thread", worker_threads = 10)] + async fn test_trade_replayer() { + let mut replayer = TradeReplayer::new(NewTradeReplayer { + fork_url: Url::from_str(CI_DEPLOY_POLYGON_RPC_URL).unwrap(), + }) + .await + .unwrap(); + + let tx_hash: Result, alloy::hex::FromHexError> = + B256::from_str("0xceb48768613542fe2a05504200caa47dc19c4e508bd70ec3b18e648eebf58177"); + + let res = replayer.replay_tx(tx_hash.unwrap()).await.unwrap(); + + let vec = vec![ + 118952503035253418u128, + 59922244745766602640u128, + 7185901000000000000u128, + 59922244745766602640u128, + 713360056497221460u128, + 1724882671000000000000000000u128, + 1724882755000000000000000000, + ]; + + let expected_stack: Vec = vec.into_iter().map(U256::from).collect(); + + assert_eq!(res.traces[1].stack, expected_stack); + assert_eq!(res.traces.len(), 16); + } +} diff --git a/lib/rain.interpreter b/lib/rain.interpreter index a29afe65b..25bd2753d 160000 --- a/lib/rain.interpreter +++ b/lib/rain.interpreter @@ -1 +1 @@ -Subproject commit a29afe65b34c94b2b6dd9b99bc33061fed5878c6 +Subproject commit 25bd2753d34ea060abdf26349b1851af2e786db4 diff --git a/tauri-app/src-tauri/Cargo.lock b/tauri-app/src-tauri/Cargo.lock index a827c490c..19a62d026 100644 --- a/tauri-app/src-tauri/Cargo.lock +++ b/tauri-app/src-tauri/Cargo.lock @@ -1254,6 +1254,12 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "aurora-engine-modexp" version = "1.1.0" @@ -1414,7 +1420,7 @@ dependencies = [ "aws-smithy-types", "bytes", "fastrand", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "http-body 1.0.1", @@ -2688,6 +2694,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cynic-introspection" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1952cb2f976ddd5cb0c7268e8827fb5676fcd0d3281afcc8fbf07d8c6bd84218" +dependencies = [ + "cynic", + "cynic-codegen", + "indenter", + "thiserror", +] + [[package]] name = "cynic-parser" version = "0.4.5" @@ -4912,6 +4930,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", + "indexmap 2.4.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "1.8.3" @@ -5162,7 +5199,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -5185,6 +5222,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2 0.4.6", "http 1.1.0", "http-body 1.0.1", "httparse", @@ -7796,7 +7834,7 @@ version = "0.0.1" dependencies = [ "alloy", "derive_builder 0.20.0", - "reqwest 0.11.27", + "reqwest 0.12.5", "serde", "serde_json", "serde_yaml", @@ -7844,7 +7882,7 @@ dependencies = [ "rain_orderbook_app_settings", "rain_orderbook_bindings", "rain_orderbook_subgraph_client", - "reqwest 0.11.27", + "reqwest 0.12.5", "serde", "serde-wasm-bindgen 0.6.5", "serde_bytes", @@ -7874,7 +7912,7 @@ dependencies = [ "rain-error-decoding", "rain_orderbook_bindings", "rain_orderbook_subgraph_client", - "reqwest 0.11.27", + "reqwest 0.12.5", "serde", "serde-wasm-bindgen 0.6.5", "serde_json", @@ -7897,8 +7935,9 @@ dependencies = [ "chrono", "cynic", "cynic-codegen", + "cynic-introspection", "rain_orderbook_bindings", - "reqwest 0.11.27", + "reqwest 0.12.5", "serde", "serde_json", "thiserror", @@ -8137,7 +8176,7 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.30", @@ -8178,9 +8217,11 @@ checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" dependencies = [ "base64 0.22.1", "bytes", + "encoding_rs", "futures-channel", "futures-core", "futures-util", + "h2 0.4.6", "http 1.1.0", "http-body 1.0.1", "http-body-util", @@ -8205,6 +8246,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper 1.0.1", + "system-configuration", "tokio", "tokio-native-tls", "tokio-rustls 0.26.0", diff --git a/tauri-app/src-tauri/src/commands/mod.rs b/tauri-app/src-tauri/src/commands/mod.rs index badde3907..3dd36ee89 100644 --- a/tauri-app/src-tauri/src/commands/mod.rs +++ b/tauri-app/src-tauri/src/commands/mod.rs @@ -8,5 +8,6 @@ pub mod dotrain_add_order_lsp; pub mod order; pub mod order_quote; pub mod order_take; +pub mod trade_debug; pub mod vault; pub mod wallet; diff --git a/tauri-app/src-tauri/src/commands/trade_debug.rs b/tauri-app/src-tauri/src/commands/trade_debug.rs new file mode 100644 index 000000000..0b476e66f --- /dev/null +++ b/tauri-app/src-tauri/src/commands/trade_debug.rs @@ -0,0 +1,51 @@ +use alloy::primitives::{B256, U256}; +use rain_orderbook_common::replays::{NewTradeReplayer, TradeReplayer}; + +use crate::error::CommandResult; + +#[tauri::command] +pub async fn debug_trade(tx_hash: String, rpc_url: String) -> CommandResult> { + let mut replayer: TradeReplayer = TradeReplayer::new(NewTradeReplayer { + fork_url: rpc_url.parse()?, + }) + .await?; + let tx_hash = tx_hash.parse::()?; + let res = replayer.replay_tx(tx_hash).await?; + let stack = res.traces[1].stack.iter().map(|x| x.clone()).collect(); + Ok(stack) +} + +#[cfg(test)] +mod tests { + use rain_orderbook_env::CI_DEPLOY_POLYGON_RPC_URL; + use std::str::FromStr; + + use super::*; + + #[tokio::test(flavor = "multi_thread", worker_threads = 10)] + async fn test_trade_replayer() { + let tx_hash: Result, alloy::hex::FromHexError> = + B256::from_str("0xceb48768613542fe2a05504200caa47dc19c4e508bd70ec3b18e648eebf58177"); + + let res = debug_trade( + tx_hash.unwrap().to_string(), + CI_DEPLOY_POLYGON_RPC_URL.to_string(), + ) + .await + .unwrap(); + + let vec = vec![ + 8255747967003398332u128, + 5195342786557434200u128, + 43067440648007307827u128, + 5195342786557434200u128, + 519534278655743420u128, + 1724872944000000000000000000u128, + 1724873082000000000000000000u128, + ]; + + let expected_stack: Vec = vec.into_iter().map(U256::from).collect(); + + assert_eq!(res, expected_stack); + } +} diff --git a/tauri-app/src-tauri/src/error.rs b/tauri-app/src-tauri/src/error.rs index d47ca8c97..1160eae56 100644 --- a/tauri-app/src-tauri/src/error.rs +++ b/tauri-app/src-tauri/src/error.rs @@ -1,6 +1,7 @@ +use alloy::hex::FromHexError; use alloy::primitives::ruint::{FromUintError, ParseError as FromUintParseError}; use alloy_ethers_typecast::{client::LedgerClientError, transaction::ReadableClientError}; -use dotrain::error::ComposeError; +use dotrain::error::{self, ComposeError}; use rain_orderbook_app_settings::config::ParseConfigSourceError; use rain_orderbook_app_settings::config_source::ConfigSourceError; use rain_orderbook_app_settings::merge::MergeError; @@ -90,6 +91,12 @@ pub enum CommandError { #[error(transparent)] FromUintParseError(#[from] FromUintParseError), + + #[error(transparent)] + FromHexError(#[from] FromHexError), + + #[error(transparent)] + TradeReplayerError(#[from] rain_orderbook_common::replays::TradeReplayerError), } impl Serialize for CommandError { diff --git a/tauri-app/src-tauri/src/main.rs b/tauri-app/src-tauri/src/main.rs index a52a1e114..29a2500f8 100644 --- a/tauri-app/src-tauri/src/main.rs +++ b/tauri-app/src-tauri/src/main.rs @@ -19,6 +19,7 @@ use commands::order::{ }; use commands::order_quote::batch_order_quotes; use commands::order_take::{order_takes_list, order_takes_list_write_csv}; +use commands::trade_debug::debug_trade; use commands::vault::{ vault_balance_changes_list, vault_balance_changes_list_write_csv, vault_deposit, vault_deposit_approve_calldata, vault_deposit_calldata, vault_detail, vault_withdraw, @@ -74,6 +75,7 @@ fn run_tauri_app() { get_authoring_meta_v2_for_scenarios, compose_from_scenario, batch_order_quotes, + debug_trade, get_app_commit_sha, validate_raindex_version ]) diff --git a/tauri-app/src/lib/components/modal/ModalTradeDebug.svelte b/tauri-app/src/lib/components/modal/ModalTradeDebug.svelte new file mode 100644 index 000000000..db95e5f18 --- /dev/null +++ b/tauri-app/src/lib/components/modal/ModalTradeDebug.svelte @@ -0,0 +1,68 @@ + + + +
+ Trade transaction: {txHash} + RPC: {rpcUrl} +
+ {#if $debugQuery.isLoading} +
+ + Replaying trade... this can take a while. +
+ {/if} + {#if $debugQuery.isError} + {$debugQuery.error} + {/if} + {#if $debugQuery.data} + + + Stack item + Value + Hex + + + {#each $debugQuery.data as value, i} + + {i} + {formatEther(hexToBigInt(value))} + {value} + + {/each} + +
+ {/if} +
diff --git a/tauri-app/src/lib/components/modal/ModalTradeDebug.test.ts b/tauri-app/src/lib/components/modal/ModalTradeDebug.test.ts new file mode 100644 index 000000000..98ae78ae8 --- /dev/null +++ b/tauri-app/src/lib/components/modal/ModalTradeDebug.test.ts @@ -0,0 +1,46 @@ +import { render, screen, waitFor } from '@testing-library/svelte'; +import { test } from 'vitest'; +import { expect } from '$lib/test/matchers'; +import { mockIPC } from '@tauri-apps/api/mocks'; +import ModalTradeDebug from './ModalTradeDebug.svelte'; +import { QueryClient } from '@tanstack/svelte-query'; +import { mockTradeDebug } from '$lib/queries/tradeDebug'; +import { formatEther } from 'viem'; + +test('renders table with the correct data', async () => { + const queryClient = new QueryClient(); + + mockIPC((cmd) => { + if (cmd === 'debug_trade') { + return mockTradeDebug; + } + }); + + render(ModalTradeDebug, { + context: new Map([['$$_queryClient', queryClient]]), + props: { open: true, txHash: '0x123', rpcUrl: 'https://rpc-url.com' }, + }); + + expect(await screen.findByText('Debug trade')).toBeInTheDocument(); + expect(await screen.findByTestId('modal-trade-debug-loading-message')).toBeInTheDocument(); + + await waitFor(() => { + expect(screen.queryByTestId('modal-trade-debug-tx-hash')).toHaveTextContent( + 'Trade transaction: 0x123', + ); + expect(screen.queryByTestId('modal-trade-debug-rpc-url')).toHaveTextContent( + 'RPC: https://rpc-url.com', + ); + }); + + const stacks = await screen.findAllByTestId('modal-trade-debug-stack'); + expect(stacks).toHaveLength(3); + const values = await screen.findAllByTestId('modal-trade-debug-value'); + expect(values).toHaveLength(3); + const hexValues = await screen.findAllByTestId('modal-trade-debug-value-hex'); + for (let i = 0; i < 3; i++) { + expect(stacks[i]).toHaveTextContent(i.toString()); + expect(values[i]).toHaveTextContent(formatEther(BigInt(mockTradeDebug[i]))); + expect(hexValues[i]).toHaveTextContent(mockTradeDebug[i]); + } +}); diff --git a/tauri-app/src/lib/components/tables/OrderTradesListTable.svelte b/tauri-app/src/lib/components/tables/OrderTradesListTable.svelte index 6917dadfb..71bf570ef 100644 --- a/tauri-app/src/lib/components/tables/OrderTradesListTable.svelte +++ b/tauri-app/src/lib/components/tables/OrderTradesListTable.svelte @@ -3,13 +3,15 @@ import TanstackAppTable from './TanstackAppTable.svelte'; import { QKEY_ORDER_TRADES_LIST } from '$lib/queries/keys'; import { orderTradesList } from '$lib/queries/orderTradesList'; - import { subgraphUrl } from '$lib/stores/settings'; + import { rpcUrl, subgraphUrl } from '$lib/stores/settings'; import { DEFAULT_PAGE_SIZE } from '$lib/queries/constants'; import { TableBodyCell, TableHeadCell } from 'flowbite-svelte'; import { formatTimestampSecondsAsLocal } from '$lib/utils/time'; import Hash from '$lib/components/Hash.svelte'; import { HashType } from '$lib/types/hash'; import { formatUnits } from 'viem'; + import { handleDebugTradeModal } from '$lib/services/modal'; + import { BugOutline } from 'flowbite-svelte-icons'; export let id: string; @@ -35,6 +37,7 @@ Input Output IO Ratio + @@ -79,5 +82,16 @@ {item.input_vault_balance_change.vault.token.symbol}/{item.output_vault_balance_change.vault .token.symbol} + + + diff --git a/tauri-app/src/lib/components/tables/OrderTradesListTable.test.ts b/tauri-app/src/lib/components/tables/OrderTradesListTable.test.ts index 31f07dff2..6ee3810f4 100644 --- a/tauri-app/src/lib/components/tables/OrderTradesListTable.test.ts +++ b/tauri-app/src/lib/components/tables/OrderTradesListTable.test.ts @@ -24,6 +24,14 @@ vi.mock('$lib/stores/settings', async (importOriginal) => { }; }); +vi.mock('$lib/services/modal', async () => { + return { + handleDepositGenericModal: vi.fn(), + handleDepositModal: vi.fn(), + handleWithdrawModal: vi.fn(), + }; +}); + const mockTakeOrdersList: Trade[] = [ { id: '1', @@ -142,3 +150,23 @@ test('renders table with correct data', async () => { } }); }); + +test('renders a debug button for each trade', async () => { + const queryClient = new QueryClient(); + + mockIPC((cmd) => { + if (cmd === 'order_takes_list') { + return mockTakeOrdersList; + } + }); + + render(OrderTradesListTable, { + context: new Map([['$$_queryClient', queryClient]]), + props: { id: '1' }, + }); + + await waitFor(async () => { + const buttons = screen.getAllByTestId('debug-trade-button'); + expect(buttons).toHaveLength(mockTakeOrdersList.length); + }); +}); diff --git a/tauri-app/src/lib/queries/queryClient.ts b/tauri-app/src/lib/queries/queryClient.ts new file mode 100644 index 000000000..71972319b --- /dev/null +++ b/tauri-app/src/lib/queries/queryClient.ts @@ -0,0 +1,10 @@ +import { browser } from '$app/environment'; +import { QueryClient } from '@tanstack/svelte-query'; + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + enabled: browser, + }, + }, +}); diff --git a/tauri-app/src/lib/queries/tradeDebug.ts b/tauri-app/src/lib/queries/tradeDebug.ts new file mode 100644 index 000000000..0fcc60fd1 --- /dev/null +++ b/tauri-app/src/lib/queries/tradeDebug.ts @@ -0,0 +1,27 @@ +import { invoke } from '@tauri-apps/api'; +import { mockIPC } from '@tauri-apps/api/mocks'; +import type { Hex } from 'viem'; + +export const tradeDebug = async (txHash: string, rpcUrl: string) => { + return await invoke('debug_trade', { + txHash, + rpcUrl, + }); +}; + +export const mockTradeDebug = ['0x01', '0x02', '0x03']; + +if (import.meta.vitest) { + const { it, expect } = import.meta.vitest; + + it('uses the trade_debug command correctly', async () => { + mockIPC((cmd) => { + if (cmd === 'debug_trade') { + return mockTradeDebug; + } + }); + + const result = await tradeDebug('0x123', 'https://rpc-url.com'); + expect(result).toEqual(mockTradeDebug); + }); +} diff --git a/tauri-app/src/lib/services/modal.ts b/tauri-app/src/lib/services/modal.ts index b676a4ca2..f27dfb276 100644 --- a/tauri-app/src/lib/services/modal.ts +++ b/tauri-app/src/lib/services/modal.ts @@ -5,6 +5,7 @@ import type { Vault } from '$lib/typeshare/vaultsList'; import ModalOrderRemove from '$lib/components/modal/ModalOrderRemove.svelte'; import type { Order as OrderDetailOrder } from '$lib/typeshare/orderDetail'; import type { Order as OrderListOrder } from '$lib/typeshare/ordersList'; +import ModalTradeDebug from '$lib/components/modal/ModalTradeDebug.svelte'; export const handleDepositGenericModal = () => { new ModalVaultDepositGeneric({ target: document.body, props: { open: true } }); @@ -21,3 +22,7 @@ export const handleWithdrawModal = (vault: Vault) => { export const handleOrderRemoveModal = (order: OrderDetailOrder | OrderListOrder) => { new ModalOrderRemove({ target: document.body, props: { order } }); }; + +export const handleDebugTradeModal = (txHash: string, rpcUrl: string) => { + new ModalTradeDebug({ target: document.body, props: { open: true, txHash, rpcUrl } }); +}; diff --git a/tauri-app/src/routes/+layout.svelte b/tauri-app/src/routes/+layout.svelte index f84222c51..cc7ed59cf 100644 --- a/tauri-app/src/routes/+layout.svelte +++ b/tauri-app/src/routes/+layout.svelte @@ -12,16 +12,8 @@ import { transactionStatusNoticesList } from '$lib/stores/transactionStatusNotice'; import TransactionStatusNotice from '$lib/components/TransactionStatusNotice.svelte'; import WindowDraggableArea from '$lib/components/WindowDraggableArea.svelte'; - import { browser } from '$app/environment'; - import { QueryClient, QueryClientProvider } from '@tanstack/svelte-query'; - - const queryClient = new QueryClient({ - defaultOptions: { - queries: { - enabled: browser, - }, - }, - }); + import { QueryClientProvider } from '@tanstack/svelte-query'; + import { queryClient } from '$lib/queries/queryClient';