diff --git a/crates/common/src/add_order.rs b/crates/common/src/add_order.rs index 97ed8d675..ede823d0a 100644 --- a/crates/common/src/add_order.rs +++ b/crates/common/src/add_order.rs @@ -446,6 +446,7 @@ price: 2e18; runs: None, blocks: None, deployer: deployer_arc.clone(), + entrypoint: None, }; let token1 = Token { address: Address::default(), @@ -544,6 +545,7 @@ _ _: 0 0; runs: None, blocks: None, deployer: deployer_arc.clone(), + entrypoint: None, }; let token1 = Token { address: Address::default(), @@ -676,6 +678,7 @@ _ _: 0 0; runs: None, blocks: None, deployer: deployer_arc.clone(), + entrypoint: None, }; let token1 = Token { address: Address::default(), diff --git a/crates/common/src/fuzz/mod.rs b/crates/common/src/fuzz/mod.rs index 771cdacec..c084f03fe 100644 --- a/crates/common/src/fuzz/mod.rs +++ b/crates/common/src/fuzz/mod.rs @@ -14,10 +14,9 @@ pub use rain_interpreter_eval::trace::{ use rain_interpreter_eval::{ error::ForkCallError, eval::ForkEvalArgs, fork::Forker, trace::RainEvalResult, }; -use rain_orderbook_app_settings::blocks::BlockError; -use rain_orderbook_app_settings::chart::Chart; -use rain_orderbook_app_settings::config::*; -use rain_orderbook_app_settings::scenario::Scenario; +use rain_orderbook_app_settings::{ + blocks::BlockError, chart::Chart, config::*, order::OrderIO, scenario::Scenario, +}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::sync::Arc; @@ -31,6 +30,23 @@ pub struct ChartData { charts: HashMap, } +#[typeshare] +#[derive(Debug, Serialize, Deserialize)] +pub struct DeploymentDebugData { + pub result: HashMap>, + #[typeshare(typescript(type = "string"))] + pub block_number: U256, +} +#[typeshare] +#[derive(Debug, Serialize, Deserialize)] +pub struct DeploymentDebugPairData { + pub order: String, + pub scenario: String, + pub pair: String, + pub result: Option, + pub error: Option, +} + #[derive(Debug)] pub struct FuzzResult { pub scenario: String, @@ -67,6 +83,10 @@ pub struct FuzzRunner { pub enum FuzzRunnerError { #[error("Scenario not found")] ScenarioNotFound(String), + #[error("Deployment not found")] + DeploymentNotFound(String), + #[error("Order not found")] + OrderNotFound, #[error("Scenario has no runs defined")] ScenarioNoRuns, #[error("Corrupt traces")] @@ -125,6 +145,17 @@ impl FuzzRunner { let deployer = scenario.deployer.clone(); + // Create entrypoints from the scenario's entrypoint field + let entrypoints: Vec = if let Some(entrypoint) = &scenario.entrypoint { + vec![entrypoint.clone()] + } else { + ORDERBOOK_ORDER_ENTRYPOINTS + .iter() + .map(|&s| s.to_string()) + .collect() + }; + let entrypoints = Arc::new(entrypoints); + // Fetch the latest block number let block_number = ReadableClientHttp::new_from_url(deployer.network.rpc.to_string())? .get_block_number() @@ -188,6 +219,7 @@ impl FuzzRunner { let deployer = Arc::clone(&deployer); let scenario_bindings = scenario_bindings.clone(); let dotrain = Arc::clone(&dotrain); + let entrypoints = Arc::clone(&entrypoints); let mut final_bindings: Vec = vec![]; @@ -204,7 +236,7 @@ impl FuzzRunner { let rainlang_string = RainDocument::compose_text( &dotrain, - &ORDERBOOK_ORDER_ENTRYPOINTS, + &entrypoints.iter().map(AsRef::as_ref).collect::>(), None, Some(final_bindings), )?; @@ -245,6 +277,128 @@ impl FuzzRunner { }) } + pub async fn run_debug( + &mut self, + block_number: u64, + input: OrderIO, + output: OrderIO, + scenario: &Arc, + ) -> Result { + let deployer = scenario.deployer.clone(); + + // Create a fork with the first block number + self.forker + .add_or_select( + NewForkedEvm { + fork_url: deployer.network.rpc.clone().into(), + fork_block_number: Some(block_number), + }, + None, + ) + .await?; + + // Pull out the bindings from the scenario + let scenario_bindings: Vec = scenario + .bindings + .clone() + .into_iter() + .map(|(k, v)| Rebind(k, v)) + .collect(); + + // Create a new RainDocument with the dotrain and the bindings + // The bindings in the dotrain string are ignored by the RainDocument + let rain_document = RainDocument::create( + self.dotrain.clone(), + None, + None, + Some(scenario_bindings.clone()), + ); + + // Search the namespace hash map for NamespaceItems that are elided and make a vec of the keys + let elided_binding_keys = Arc::new( + rain_document + .namespace() + .iter() + .filter(|(_, v)| v.is_elided_binding()) + .map(|(k, _)| k.clone()) + .collect::>(), + ); + + let dotrain = Arc::new(self.dotrain.clone()); + self.forker.roll_fork(Some(block_number), None)?; + let fork = Arc::new(self.forker.clone()); // Wrap in Arc for shared ownership + let fork_clone = Arc::clone(&fork); // Clone the Arc for each thread + let elided_binding_keys = Arc::clone(&elided_binding_keys); + let deployer = Arc::clone(&deployer); + let scenario_bindings = scenario_bindings.clone(); + let dotrain = Arc::clone(&dotrain); + + let mut final_bindings: Vec = vec![]; + + // For each scenario.fuzz_binds, add a random value + for elided_binding in elided_binding_keys.as_slice() { + let mut val: [u8; 32] = [0; 32]; + self.rng.fill_bytes(&mut val); + let hex = alloy::primitives::hex::encode_prefixed(val); + final_bindings.push(Rebind(elided_binding.to_string(), hex)); + } + + let handle = tokio::spawn(async move { + final_bindings.extend(scenario_bindings.clone()); + + let rainlang_string = RainDocument::compose_text( + &dotrain, + &ORDERBOOK_ORDER_ENTRYPOINTS, + None, + Some(final_bindings), + )?; + + // Create a 5x5 grid of zero values for context - later we'll + // replace these with sane values based on Orderbook context + let mut context = vec![vec![U256::from(0); 5]; 5]; + // set random hash for context order hash cell + context[1][0] = rand::random(); + + // set input values in context + // input token + context[3][0] = U256::from_be_slice(input.token.address.0.as_slice()); + // input decimals + context[3][1] = U256::from(input.token.decimals.unwrap_or(18)); + // input vault id + context[3][2] = input.vault_id.unwrap_or(U256::from(0)); + // input vault balance before + context[3][3] = U256::from(0); + + // set output values in context + // output token + context[4][0] = U256::from_be_slice(output.token.address.0.as_slice()); + // output decimals + context[4][1] = U256::from(output.token.decimals.unwrap_or(18)); + // output vault id + context[4][2] = output.vault_id.unwrap_or(U256::from(0)); + // output vault balance before + context[4][3] = U256::from(0); + + let args = ForkEvalArgs { + rainlang_string, + source_index: 0, + deployer: deployer.address, + namespace: FullyQualifiedNamespace::default(), + context, + decode_errors: true, + }; + fork_clone + .fork_eval(args) + .map_err(FuzzRunnerError::ForkCallError) + .await + }); + + Ok(FuzzResult { + scenario: scenario.name.clone(), + runs: vec![handle.await??.into()].into(), + }) + } + pub async fn make_chart_data(&self) -> Result { let charts = self.settings.charts.clone(); let mut scenarios_data: HashMap = HashMap::new(); @@ -270,17 +424,101 @@ impl FuzzRunner { charts, }) } + + pub async fn make_debug_data( + &self, + block_number: Option, + ) -> Result { + let mut block = block_number.unwrap_or(0); + let mut pair_datas: HashMap> = HashMap::new(); + + let deployments = self.settings.deployments.clone(); + + for (deployment_name, deployment) in deployments.clone() { + let scenario = deployment.scenario.clone(); + + if block_number.is_none() { + // Fetch the latest block number + block = + ReadableClientHttp::new_from_url(scenario.deployer.network.rpc.to_string())? + .get_block_number() + .await?; + } + + let order_name = self + .settings + .orders + .iter() + .find(|(_, order)| *order == &deployment.order) + .map(|(key, _)| key.clone()) + .ok_or(FuzzRunnerError::OrderNotFound)?; + + for input in &deployment.order.inputs { + for output in &deployment.order.outputs { + if input.token.address != output.token.address { + let pair = format!( + "{}/{}", + input.token.symbol.clone().unwrap_or("UNKNOWN".to_string()), + output.token.symbol.clone().unwrap_or("UNKNOWN".to_string()) + ); + + let mut pair_data = DeploymentDebugPairData { + order: order_name.clone(), + scenario: scenario.name.clone(), + pair, + result: None, + error: None, + }; + + let mut runner = self.clone(); + match runner + .run_debug(block, input.clone(), output.clone(), &scenario) + .await + { + Ok(fuzz_result) => match fuzz_result.flatten_traces() { + Ok(fuzz_result) => { + pair_data.result = Some(fuzz_result); + } + Err(e) => { + pair_data.error = Some(e.to_string()); + } + }, + Err(e) => { + if matches!(e, FuzzRunnerError::ComposeError(_)) { + return Err(e); + } + pair_data.error = Some(e.to_string()); + } + } + + pair_datas + .entry(deployment_name.clone()) + .or_default() + .push(pair_data); + } + } + } + } + + let result = DeploymentDebugData { + result: pair_datas, + block_number: U256::from(block), + }; + + Ok(result) + } } #[cfg(test)] mod tests { use super::*; use alloy::{ - primitives::utils::parse_ether, + primitives::{utils::parse_ether, Address}, providers::{ext::AnvilApi, Provider}, }; use rain_orderbook_app_settings::config_source::ConfigSource; use rain_orderbook_test_fixtures::LocalEvm; + use std::str::FromStr; #[tokio::test(flavor = "multi_thread", worker_threads = 10)] async fn test_fuzz_runner() { @@ -613,4 +851,166 @@ _: context<1 0>(); } } } + + #[tokio::test(flavor = "multi_thread", worker_threads = 10)] + async fn test_custom_entrypoint() { + let local_evm = LocalEvm::new().await; + let dotrain = format!( + r#" +deployers: + some-key: + address: {deployer} +networks: + some-key: + rpc: {rpc_url} + chain-id: 123 +scenarios: + some-key: + runs: 1 + entrypoint: "some-entrypoint" +charts: + some-key: + scenario: some-key +--- +#some-entrypoint +a: 20, +b: 30; + "#, + rpc_url = local_evm.url(), + deployer = local_evm.deployer.address() + ); + let frontmatter = RainDocument::get_front_matter(&dotrain).unwrap(); + let settings = serde_yaml::from_str::(frontmatter).unwrap(); + let config = settings + .try_into() + .map_err(|e| println!("{:?}", e)) + .unwrap(); + + let runner = FuzzRunner::new(&dotrain, config, None).await; + + let res = runner + .make_chart_data() + .await + .map_err(|e| println!("{:#?}", e)) + .unwrap(); + + let scenario_data = res.scenarios_data.get("some-key").unwrap(); + assert_eq!( + scenario_data.data.rows[0][0], + U256::from(20000000000000000000u128) + ); + assert_eq!( + scenario_data.data.rows[0][1], + U256::from(30000000000000000000u128) + ); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 10)] + async fn test_debug() { + let local_evm = LocalEvm::new().await; + let dotrain = format!( + r#" +deployers: + flare: + address: {deployer} +networks: + flare: + rpc: {rpc_url} + chain-id: 123 +tokens: + wflr: + network: flare + address: 0x1D80c49BbBCd1C0911346656B529DF9E5c2F783d + decimals: 18 + usdce: + network: flare + address: 0xFbDa5F676cB37624f28265A144A48B0d6e87d3b6 + decimals: 6 +scenarios: + flare: + deployer: flare + runs: 1 + bindings: + orderbook-subparser: {orderbook_subparser} +orders: + sell-wflr: + network: flare + inputs: + - token: usdce + vault-id: 10 + outputs: + - token: wflr + vault-id: 20 +deployments: + sell-wflr: + order: sell-wflr + scenario: flare +--- +#orderbook-subparser ! + +#calculate-io +using-words-from orderbook-subparser + +_: input-token(), +_: input-token-decimals(), +_: input-vault-id(), +_: output-token(), +_: output-token-decimals(), +_: output-vault-id(), + +max-output: 30, +io-ratio: mul(0.99 20); +#handle-io +:; + "#, + rpc_url = local_evm.url(), + deployer = local_evm.deployer.address(), + orderbook_subparser = local_evm.orderbook_subparser.address() + ); + + let frontmatter = RainDocument::get_front_matter(&dotrain).unwrap(); + let settings = serde_yaml::from_str::(frontmatter).unwrap(); + let config = settings + .try_into() + .map_err(|e| println!("{:?}", e)) + .unwrap(); + + let runner = FuzzRunner::new(&dotrain, config, None).await; + + let res = runner + .make_debug_data(None) + .await + .map_err(|e| println!("{:#?}", e)) + .unwrap(); + + let result_rows = res.result["sell-wflr"][0] + .result + .as_ref() + .unwrap() + .data + .rows[0] + .clone(); + assert_eq!( + result_rows[0], + U256::from_be_slice( + Address::from_str("0xFbDa5F676cB37624f28265A144A48B0d6e87d3b6") + .unwrap() + .0 + .as_slice() + ) + ); + assert_eq!(result_rows[1], U256::from(6)); + assert_eq!(result_rows[2], U256::from(10)); + assert_eq!( + result_rows[3], + U256::from_be_slice( + Address::from_str("0x1D80c49BbBCd1C0911346656B529DF9E5c2F783d") + .unwrap() + .0 + .as_slice() + ) + ); + assert_eq!(result_rows[4], U256::from(18)); + assert_eq!(result_rows[5], U256::from(20)); + } } diff --git a/crates/settings/src/chart.rs b/crates/settings/src/chart.rs index 8224cb2eb..2700bc880 100644 --- a/crates/settings/src/chart.rs +++ b/crates/settings/src/chart.rs @@ -93,6 +93,7 @@ mod tests { bindings: HashMap::from([(String::from("key"), String::from("value"))]), // Example binding runs, blocks: None, + entrypoint: None, deployer: mock_deployer(), }; (name.to_string(), Arc::new(scenario)) diff --git a/crates/settings/src/config_source.rs b/crates/settings/src/config_source.rs index 0513a3fe4..113709db2 100644 --- a/crates/settings/src/config_source.rs +++ b/crates/settings/src/config_source.rs @@ -176,6 +176,8 @@ pub struct ScenarioConfigSource { #[serde(skip_serializing_if = "Option::is_none")] pub blocks: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub entrypoint: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub deployer: Option, #[serde(skip_serializing_if = "Option::is_none")] pub scenarios: Option>, diff --git a/crates/settings/src/deployment.rs b/crates/settings/src/deployment.rs index 794b79fc9..ac821b2ad 100644 --- a/crates/settings/src/deployment.rs +++ b/crates/settings/src/deployment.rs @@ -76,6 +76,7 @@ mod tests { deployer: mock_deployer(), runs: None, blocks: None, + entrypoint: None, }; let order = Order { inputs: vec![], @@ -105,6 +106,7 @@ mod tests { deployer: mock_deployer(), runs: None, blocks: None, + entrypoint: None, }; let order = Order { inputs: vec![], diff --git a/crates/settings/src/scenario.rs b/crates/settings/src/scenario.rs index 6865ce758..6605a49cb 100644 --- a/crates/settings/src/scenario.rs +++ b/crates/settings/src/scenario.rs @@ -20,6 +20,7 @@ pub struct Scenario { pub runs: Option, #[typeshare(skip)] pub blocks: Option, + pub entrypoint: Option, #[typeshare(typescript(type = "Deployer"))] pub deployer: Arc, } @@ -103,6 +104,7 @@ impl ScenarioConfigSource { bindings: bindings.clone(), runs: self.runs, blocks: self.blocks.clone(), + entrypoint: self.entrypoint.clone(), deployer: deployer_ref.clone(), }); @@ -175,6 +177,7 @@ mod tests { bindings: HashMap::new(), // Assuming no bindings for simplification runs: Some(2), blocks: None, + entrypoint: None, deployer: None, scenarios: None, // No further nesting }, @@ -187,6 +190,7 @@ mod tests { bindings: HashMap::new(), // Assuming no bindings for simplification runs: Some(5), blocks: None, + entrypoint: None, deployer: None, scenarios: Some(nested_scenario2), // Include nested_scenario2 }, @@ -200,6 +204,7 @@ mod tests { bindings: HashMap::new(), // Assuming no bindings for simplification runs: Some(10), blocks: None, + entrypoint: None, deployer: Some("mainnet".to_string()), scenarios: Some(nested_scenario1), // Include nested_scenario1 }, @@ -274,6 +279,7 @@ mod tests { bindings: child_bindings, runs: None, blocks: None, + entrypoint: None, deployer: None, scenarios: None, }; diff --git a/tauri-app/src-tauri/src/commands/charts.rs b/tauri-app/src-tauri/src/commands/charts.rs index 1298517c7..cd67b0271 100644 --- a/tauri-app/src-tauri/src/commands/charts.rs +++ b/tauri-app/src-tauri/src/commands/charts.rs @@ -10,4 +10,17 @@ pub async fn make_charts(dotrain: String, settings: String) -> CommandResult, +) -> CommandResult { + let config = merge_configstrings(dotrain.clone(), settings).await?; + let final_config: Config = config.try_into()?; + let fuzzer = FuzzRunner::new(dotrain.as_str(), final_config.clone(), None).await; + + Ok(fuzzer.make_debug_data(block_number).await?) +} diff --git a/tauri-app/src-tauri/src/main.rs b/tauri-app/src-tauri/src/main.rs index 71750fafd..3046567bd 100644 --- a/tauri-app/src-tauri/src/main.rs +++ b/tauri-app/src-tauri/src/main.rs @@ -9,7 +9,7 @@ mod commands; use commands::app::get_app_commit_sha; use commands::authoring_meta::get_authoring_meta_v2_for_scenarios; use commands::chain::{get_block_number, get_chainid}; -use commands::charts::make_charts; +use commands::charts::{make_charts, make_deployment_debug}; use commands::config::{convert_configstring_to_config, merge_configstrings, parse_configstring}; use commands::dotrain::parse_dotrain; use commands::dotrain_add_order_lsp::{call_lsp_completion, call_lsp_hover, call_lsp_problems}; @@ -68,6 +68,7 @@ fn run_tauri_app() { merge_configstrings, convert_configstring_to_config, make_charts, + make_deployment_debug, order_add_calldata, order_remove_calldata, vault_deposit_approve_calldata, diff --git a/tauri-app/src/lib/components/EditableSpan.svelte b/tauri-app/src/lib/components/EditableSpan.svelte index 1d9ac1ce4..08f51f167 100644 --- a/tauri-app/src/lib/components/EditableSpan.svelte +++ b/tauri-app/src/lib/components/EditableSpan.svelte @@ -1,5 +1,6 @@ -{#if chartData} -
- {#each Object.values(chartData.scenarios_data) as scenario} - {@const data = transformData(scenario)} -
- {scenario.scenario} - - - Stack item - Value - Hex - - - {#each Object.entries(data[0]) as [key, value]} - - {key} - {value[0]} - {value[1]} - - {/each} - -
-
- {/each} +
+
+ {#if debugError} +
{debugError}
+ {/if} + {#if $data && isHex($data.block_number)} + { + enabled = false; + }} + on:blur={({ detail: { value } }) => { + blockNumber = parseInt(value); + handleRefresh(); + }} + /> + {/if} + + {#if data} + + { + enabled = false; + }} + /> + { + enabled = true; + blockNumber = undefined; + handleRefresh(); + }} + class={`ml-2 h-8 w-3 cursor-pointer text-gray-400 dark:text-gray-400 ${enabled ? 'hidden' : ''}`} + /> + {/if}
-{:else} - No scenario data -{/if} +
+ +{#each Object.entries($data?.result ?? {}) as [deploymentName, results]} +

Deployment: {deploymentName}

+ + + Order + Scenario + Pair + Maximum Output + Ratio + + + + + {#each results as item} + + {item.order} + {item.scenario} + {item.pair} + {#if item.result} + {@const fuzzResult = item.result} + {@const data = transformData(fuzzResult)[0]} + {@const dataEntries = Object.entries(data)} + {#if dataEntries.length === 1} + Missing stack data for max output and ratio + {:else} + {@const maxOutput = dataEntries[dataEntries.length - 2]} + {@const ioRatio = dataEntries[dataEntries.length - 1]} + + {maxOutput[1][0]} + + + {ioRatio[1][0]} + + ({BigInt(ioRatio[1][1]) === 0n + ? '0' + : formatUnits(10n ** 36n / BigInt(ioRatio[1][1]), 18)}) + + + {/if} + + + + {:else} + {item.error} + {/if} + + {/each} + +
+{/each} diff --git a/tauri-app/src/lib/components/modal/ModalScenarioDebug.svelte b/tauri-app/src/lib/components/modal/ModalScenarioDebug.svelte new file mode 100644 index 000000000..997e7d294 --- /dev/null +++ b/tauri-app/src/lib/components/modal/ModalScenarioDebug.svelte @@ -0,0 +1,17 @@ + + + +
+
+
+ +
+
diff --git a/tauri-app/src/lib/services/chart.ts b/tauri-app/src/lib/services/chart.ts index aeb59bb03..6231f9c4b 100644 --- a/tauri-app/src/lib/services/chart.ts +++ b/tauri-app/src/lib/services/chart.ts @@ -1,5 +1,12 @@ -import type { ChartData } from '$lib/typeshare/config'; +import type { ChartData, DeploymentDebugData } from '$lib/typeshare/config'; import { invoke } from '@tauri-apps/api'; export const makeChartData = async (dotrain: string, settings: string): Promise => invoke('make_charts', { dotrain, settings }); + +export const makeDeploymentDebugData = async ( + dotrain: string, + settings: string, + blockNumber?: number, +): Promise => + invoke('make_deployment_debug', { dotrain, settings, blockNumber }); diff --git a/tauri-app/src/lib/services/modal.ts b/tauri-app/src/lib/services/modal.ts index f397b1513..7a89f8e59 100644 --- a/tauri-app/src/lib/services/modal.ts +++ b/tauri-app/src/lib/services/modal.ts @@ -8,6 +8,8 @@ import type { Order as OrderListOrder } from '$lib/typeshare/subgraphTypes'; import ModalTradeDebug from '$lib/components/modal/ModalTradeDebug.svelte'; import type { Hex } from 'viem'; import ModalQuoteDebug from '$lib/components/modal/ModalQuoteDebug.svelte'; +import ModalScenarioDebug from '$lib/components/modal/ModalScenarioDebug.svelte'; +import type { RainEvalResultsTable } from '$lib/typeshare/config'; export const handleDepositGenericModal = () => { new ModalVaultDepositGeneric({ target: document.body, props: { open: true } }); @@ -55,3 +57,7 @@ export const handleQuoteDebugModal = ( }, }); }; + +export const handleScenarioDebugModal = (pair: string, data: RainEvalResultsTable) => { + new ModalScenarioDebug({ target: document.body, props: { open: true, pair, data } }); +}; diff --git a/tauri-app/src/routes/orders/add/+page.svelte b/tauri-app/src/routes/orders/add/+page.svelte index 303deed23..1e3bc7cff 100644 --- a/tauri-app/src/routes/orders/add/+page.svelte +++ b/tauri-app/src/routes/orders/add/+page.svelte @@ -256,10 +256,6 @@ - - {/if} - + + + - +
+ + +