diff --git a/Cargo.lock b/Cargo.lock index 5af20ec4..9f0b9ba5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4753,15 +4753,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "serde_spanned" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" -dependencies = [ - "serde", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -4991,10 +4982,11 @@ dependencies = [ "aws-sdk-kms", "hex", "mockall", + "serde", + "serde_json", "solver-types", "thiserror 2.0.17", "tokio", - "toml", ] [[package]] @@ -5005,11 +4997,11 @@ dependencies = [ "regex", "rust_decimal", "serde", + "serde_json", "solver-types", "tempfile", "thiserror 2.0.17", "tokio", - "toml", ] [[package]] @@ -5037,7 +5029,6 @@ dependencies = [ "solver-types", "thiserror 2.0.17", "tokio", - "toml", "tracing", ] @@ -5066,7 +5057,6 @@ dependencies = [ "solver-types", "thiserror 2.0.17", "tokio", - "toml", "tracing", ] @@ -5104,13 +5094,14 @@ dependencies = [ "serde_json", "solver-account", "solver-config", + "solver-service", + "solver-storage", "solver-types", "sysinfo", "tempfile", "thiserror 2.0.17", "tokio", "tokio-test", - "toml", "tracing", "tracing-subscriber", "url", @@ -5142,7 +5133,6 @@ dependencies = [ "solver-types", "thiserror 2.0.17", "tokio", - "toml", "tower", "tower-http", "tracing", @@ -5164,7 +5154,6 @@ dependencies = [ "solver-types", "thiserror 2.0.17", "tokio", - "toml", "tracing", "uuid", ] @@ -5183,7 +5172,6 @@ dependencies = [ "solver-types", "thiserror 2.0.17", "tokio", - "toml", "tracing", ] @@ -5229,7 +5217,6 @@ dependencies = [ "tempfile", "thiserror 2.0.17", "tokio", - "toml", "tower", "tower-http", "tracing", @@ -5258,7 +5245,6 @@ dependencies = [ "solver-types", "thiserror 2.0.17", "tokio", - "toml", "tracing", ] @@ -5279,7 +5265,6 @@ dependencies = [ "tempfile", "thiserror 2.0.17", "tokio", - "toml", "tracing", "uuid", ] @@ -5306,7 +5291,6 @@ dependencies = [ "serde_json", "thiserror 2.0.17", "tokio", - "toml", "tracing", "zeroize", ] @@ -5715,36 +5699,12 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" -dependencies = [ - "indexmap 2.11.0", - "serde", - "serde_spanned", - "toml_datetime 0.7.0", - "toml_parser", - "toml_writer", - "winnow", -] - [[package]] name = "toml_datetime" version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -[[package]] -name = "toml_datetime" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" -dependencies = [ - "serde", -] - [[package]] name = "toml_edit" version = "0.22.27" @@ -5752,25 +5712,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap 2.11.0", - "toml_datetime 0.6.11", + "toml_datetime", "winnow", ] -[[package]] -name = "toml_parser" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" -dependencies = [ - "winnow", -] - -[[package]] -name = "toml_writer" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" - [[package]] name = "tower" version = "0.5.2" diff --git a/Cargo.toml b/Cargo.toml index 0c6e1e70..9435b29f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,7 +68,6 @@ tempfile = "3.0" thiserror = "2.0.12" tokio = { version = "1.40", features = ["full"] } tokio-stream = "0.1" -toml = "0.9.2" tower = "0.5.2" tower-http = { version = "0.6.6", features = ["trace", "cors"] } tracing = "0.1" diff --git a/README.md b/README.md index c4cb0b0d..f48c28c5 100644 --- a/README.md +++ b/README.md @@ -854,7 +854,7 @@ sequenceDiagram The admin API enables authorized wallet addresses to perform administrative operations using EIP-712 signed messages. This provides secure, decentralized admin access without shared secrets. -**Setup:** Configure admin addresses in your bootstrap config or TOML config: +**Setup:** Configure admin addresses in your seed overrides JSON: ```json { @@ -1101,8 +1101,8 @@ git clone https://github.com/openintentsframework/oif-contracts.git cd oif-contracts && forge build && cd .. # 1. Initialize configuration and load it -cargo run -p solver-demo -- init new config/demo.toml -cargo run -p solver-demo -- init load config/demo.toml --local +cargo run -p solver-demo -- init new config/demo.json +cargo run -p solver-demo -- init load config/demo.json --local # 2. Start local environment (Anvil chains) cargo run -p solver-demo -- env start @@ -1295,9 +1295,10 @@ The demo tool generates files in the `.oif-demo/requests/` directory following a The demo tool provides a complete workflow for setting up a test environment: -1. **Initialize Configuration** (`init new` / `init load`): +1. **Initialize Configuration** (`init new` / `init load` / `init load-storage`): - Creates or loads solver configuration + - Can load runtime config directly from storage backend (`init load-storage`) - Sets up network definitions and RPC endpoints - Configures account keys and signing - Stores session data in `.oif-demo/` directory diff --git a/config/demo.json b/config/demo.json new file mode 100644 index 00000000..da84b792 --- /dev/null +++ b/config/demo.json @@ -0,0 +1,215 @@ +{ + "account": { + "implementations": { + "local": { + "private_key": "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + } + }, + "primary": "local" + }, + "api": { + "auth": { + "access_token_expiry_hours": 1, + "enabled": true, + "issuer": "oif-solver-demo", + "jwt_secret": "${JWT_SECRET:-MySuperDuperSecureSecret123!}", + "refresh_token_expiry_hours": 720 + }, + "enabled": true, + "host": "127.0.0.1", + "implementations": { + "discovery": "offchain_eip7683" + }, + "max_request_size": 1048576, + "port": 3000, + "quote": { + "validity_seconds": 60 + }, + "timeout_seconds": 30 + }, + "delivery": { + "implementations": { + "evm_alloy": { + "network_ids": [ + 31337, + 31338 + ] + } + }, + "min_confirmations": 1 + }, + "discovery": { + "implementations": { + "offchain_eip7683": { + "api_host": "127.0.0.1", + "api_port": 8081, + "network_ids": [ + 31337 + ] + }, + "onchain_eip7683": { + "network_ids": [ + 31337, + 31338 + ], + "polling_interval_secs": 0 + } + } + }, + "gas": { + "flows": { + "eip3009_escrow": { + "claim": 60084, + "fill": 77298, + "open": 130254 + }, + "permit2_escrow": { + "claim": 60084, + "fill": 77298, + "open": 146306 + }, + "resource_lock": { + "claim": 122793, + "fill": 77298, + "open": 0 + } + } + }, + "networks": { + "31337": { + "allocator_address": "0x00000000000000000000000000000000000003ec", + "input_settler_address": "0x00000000000000000000000000000000000003e8", + "input_settler_compact_address": "0x00000000000000000000000000000000000003eb", + "output_settler_address": "0x00000000000000000000000000000000000003e9", + "rpc_urls": [ + { + "http": "http://localhost:8545", + "ws": "ws://localhost:8545" + } + ], + "the_compact_address": "0x00000000000000000000000000000000000003ea", + "tokens": [ + { + "address": "0x00000000000000000000000000000000000003ed", + "decimals": 18, + "symbol": "TOKA" + }, + { + "address": "0x00000000000000000000000000000000000003ee", + "decimals": 18, + "symbol": "TOKB" + } + ] + }, + "31338": { + "allocator_address": "0x00000000000000000000000000000000000003f5", + "input_settler_address": "0x00000000000000000000000000000000000003f1", + "input_settler_compact_address": "0x00000000000000000000000000000000000003f4", + "output_settler_address": "0x00000000000000000000000000000000000003f2", + "rpc_urls": [ + { + "http": "http://localhost:8546", + "ws": "ws://localhost:8546" + } + ], + "the_compact_address": "0x00000000000000000000000000000000000003f3", + "tokens": [ + { + "address": "0x00000000000000000000000000000000000003f6", + "decimals": 18, + "symbol": "TOKA" + }, + { + "address": "0x00000000000000000000000000000000000003f7", + "decimals": 18, + "symbol": "TOKB" + } + ] + } + }, + "order": { + "implementations": { + "eip7683": {} + }, + "strategy": { + "implementations": { + "simple": { + "max_gas_price_gwei": 100 + } + }, + "primary": "simple" + } + }, + "pricing": { + "implementations": { + "coingecko": { + "cache_duration_seconds": 60, + "custom_prices": { + "TOKA": "200.00", + "TOKB": "195.00" + }, + "rate_limit_delay_ms": 1200 + }, + "mock": {} + }, + "primary": "mock" + }, + "settlement": { + "implementations": { + "direct": { + "dispute_period_seconds": 1, + "network_ids": [ + 31337, + 31338 + ], + "oracle_selection_strategy": "First", + "oracles": { + "input": { + "31337": [ + "0x00000000000000000000000000000000000003ef" + ], + "31338": [ + "0x00000000000000000000000000000000000003f8" + ] + }, + "output": { + "31337": [ + "0x00000000000000000000000000000000000003f0" + ], + "31338": [ + "0x00000000000000000000000000000000000003f9" + ] + } + }, + "order": "eip7683", + "routes": { + "31337": [ + 31338 + ], + "31338": [ + 31337 + ] + } + } + }, + "settlement_poll_interval_seconds": 3 + }, + "solver": { + "id": "oif-solver-demo", + "min_profitability_pct": "1.0", + "monitoring_timeout_seconds": 28800 + }, + "storage": { + "cleanup_interval_seconds": 60, + "implementations": { + "file": { + "storage_path": "./data/storage", + "ttl_intents": 120, + "ttl_order_by_tx_hash": 300, + "ttl_orders": 300 + }, + "memory": {} + }, + "primary": "file" + } +} \ No newline at end of file diff --git a/config/demo.toml b/config/demo.toml deleted file mode 100644 index d4bc46f9..00000000 --- a/config/demo.toml +++ /dev/null @@ -1,138 +0,0 @@ -# OIF Solver Configuration - Generated File -# Generated with placeholder values for easy regex replacement - -include = ["demo/networks.toml", "demo/api.toml", "demo/gas.toml"] - -[solver] -id = "oif-solver-demo" -min_profitability_pct = 1.0 -monitoring_timeout_seconds = 28800 - -# ============================================================================ -# STORAGE -# ============================================================================ -[storage] -primary = "file" -cleanup_interval_seconds = 60 - -[storage.implementations.memory] -# Memory storage has no configuration - -[storage.implementations.file] -storage_path = "./data/storage" -ttl_orders = 300 # 5 minutes -ttl_intents = 120 # 2 minutes -ttl_order_by_tx_hash = 300 # 5 minutes - -[storage.implementations.redis] -redis_url = "redis://localhost:6379" -key_prefix = "oif-solver" -connection_timeout_ms = 5000 -db = 0 -ttl_orders = 300 # 5 minutes -ttl_intents = 120 # 2 minutes -ttl_order_by_tx_hash = 300 # 5 minutes -ttl_quotes = 60 # 1 minute -ttl_settlement_messages = 600 # 10 minutes - -# ============================================================================ -# ACCOUNT -# ============================================================================ -[account] -primary = "local" - -[account.implementations.local] -private_key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" - -# ============================================================================ -# DELIVERY -# ============================================================================ -[delivery] -min_confirmations = 1 - -[delivery.implementations.evm_alloy] -network_ids = [31337, 31338] - -# ============================================================================ -# DISCOVERY -# ============================================================================ -[discovery] - -[discovery.implementations.onchain_eip7683] -network_ids = [31337, 31338] -polling_interval_secs = 0 # Use WebSocket subscriptions instead of polling - -[discovery.implementations.offchain_eip7683] -api_host = "127.0.0.1" -api_port = 8081 -network_ids = [31337] - -# ============================================================================ -# ORDER -# ============================================================================ -[order] -simulate_callbacks = true -callback_whitelist = [ - "0x0001000002210514154c8bb598df835e9617c2cdcb8c84838bd329c6", # MockCallbackExecutor on Base (EIP-7930 format) -] - -[order.implementations.eip7683] - -[order.strategy] -primary = "simple" - -[order.strategy.implementations.simple] -max_gas_price_gwei = 100 - -# ============================================================================ -# PRICING -# ============================================================================ -[pricing] -primary = "mock" - -[pricing.implementations.mock] -# Uses default ETH/USD price of 4615.16 - -[pricing.implementations.coingecko] -# Free tier configuration (no API key required) -# api_key = "CG-YOUR-API-KEY-HERE" -cache_duration_seconds = 60 -rate_limit_delay_ms = 1200 - -# Custom prices for demo/test tokens (in USD) -[pricing.implementations.coingecko.custom_prices] -TOKA = "200.00" -TOKB = "195.00" - -# ============================================================================ -# SETTLEMENT -# ============================================================================ -[settlement] -settlement_poll_interval_seconds = 3 - -[settlement.implementations.direct] -order = "eip7683" -network_ids = [31337, 31338] -dispute_period_seconds = 1 -# Oracle selection strategy when multiple oracles are available (First, RoundRobin, Random) -oracle_selection_strategy = "First" - -# Oracle configuration with multiple oracle support -[settlement.implementations.direct.oracles] -# Input oracles (on origin chains) -input = { 31337 = [ - "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", -], 31338 = [ - "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853", -] } -# Output oracles (on destination chains) -output = { 31337 = [ - "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", -], 31338 = [ - "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", -] } - -# Valid routes: from origin chain -> to destination chains -[settlement.implementations.direct.routes] -31337 = [31338] -31338 = [31337] diff --git a/config/demo/api.toml b/config/demo/api.toml deleted file mode 100644 index 942a2fbc..00000000 --- a/config/demo/api.toml +++ /dev/null @@ -1,31 +0,0 @@ -# API Server Configuration -# Configures the HTTP API for receiving off-chain intents - -[api] -enabled = true -host = "127.0.0.1" -port = 3000 -timeout_seconds = 30 -max_request_size = 1048576 # 1MB - -[api.implementations] -discovery = "offchain_eip7683" - -# JWT Authentication Configuration -[api.auth] -enabled = true -jwt_secret = "${JWT_SECRET:-MySuperDuperSecureSecret123!}" -access_token_expiry_hours = 1 -refresh_token_expiry_hours = 720 # 30 days -issuer = "oif-solver-demo" - -# Quote Configuration -[api.quote] -# Quote validity duration (time for user to sign and submit quote) -validity_seconds = 60 # 1 minute - -# Fill deadline duration (time to fill outputs on destination chains) -fill_deadline_seconds = 300 # 5 minutes - -# Expires duration (time to finalize/claim on origin chain, must be > fill_deadline) -expires_seconds = 600 # 10 minutes diff --git a/config/demo/gas.toml b/config/demo/gas.toml deleted file mode 100644 index c4dd8d7c..00000000 --- a/config/demo/gas.toml +++ /dev/null @@ -1,19 +0,0 @@ -[gas] - -[gas.flows.resource_lock] -# Gas units captured by scripts/e2e/estimate_gas.sh on local anvil -open = 0 -fill = 77298 -claim = 122793 - -[gas.flows.permit2_escrow] -# Gas units captured by scripts/e2e/estimate_gas.sh on local anvil -open = 146306 -fill = 77298 -claim = 60084 - -[gas.flows.eip3009_escrow] -# Gas units captured by scripts/e2e/estimate_gas.sh on local anvil -open = 130254 -fill = 77298 -claim = 60084 diff --git a/config/demo/networks.toml b/config/demo/networks.toml deleted file mode 100644 index 551413cc..00000000 --- a/config/demo/networks.toml +++ /dev/null @@ -1,47 +0,0 @@ -# Network Configuration - Generated with Placeholders -# Defines all supported blockchain networks and their tokens - -[networks.31337] -input_settler_address = "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" -input_settler_compact_address = "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" -the_compact_address = "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" -allocator_address = "0x0165878A594ca255338adfa4d48449f69242Eb8F" -output_settler_address = "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" - -# RPC endpoints with both HTTP and WebSocket URLs for each network -[[networks.31337.rpc_urls]] -http = "http://localhost:8545" -ws = "ws://localhost:8545" - -[[networks.31337.tokens]] -address = "0x5FbDB2315678afecb367f032d93F642f64180aa3" -symbol = "TOKA" -decimals = 18 - -[[networks.31337.tokens]] -address = "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" -symbol = "TOKB" -decimals = 18 - -[networks.31338] -input_settler_address = "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" -input_settler_compact_address = "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" -the_compact_address = "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" -allocator_address = "0x0165878A594ca255338adfa4d48449f69242Eb8F" -output_settler_address = "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" - -# RPC endpoints with both HTTP and WebSocket URLs for each network -[[networks.31338.rpc_urls]] -http = "http://localhost:8546" -ws = "ws://localhost:8546" - -[[networks.31338.tokens]] -address = "0x5FbDB2315678afecb367f032d93F642f64180aa3" -symbol = "TOKA" -decimals = 18 - -[[networks.31338.tokens]] -address = "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" -symbol = "TOKB" -decimals = 18 - diff --git a/crates/solver-account/Cargo.toml b/crates/solver-account/Cargo.toml index 6cadfed7..6c4afbe3 100644 --- a/crates/solver-account/Cargo.toml +++ b/crates/solver-account/Cargo.toml @@ -13,9 +13,10 @@ alloy-signer-local = { workspace = true } async-trait = "0.1" hex = "0.4" mockall = { workspace = true, optional = true } +serde = { workspace = true } +serde_json = { workspace = true } solver-types = { path = "../solver-types" } thiserror = "2.0.17" -toml = { workspace = true } tokio = { workspace = true } # KMS dependencies (optional) diff --git a/crates/solver-account/src/implementations/kms.rs b/crates/solver-account/src/implementations/kms.rs index 4b2e926c..f55a719e 100644 --- a/crates/solver-account/src/implementations/kms.rs +++ b/crates/solver-account/src/implementations/kms.rs @@ -13,41 +13,56 @@ use alloy_primitives::{Address as AlloyAddress, Bytes, TxKind}; use alloy_signer_aws::AwsSigner; use async_trait::async_trait; use aws_sdk_kms::Client as KmsClient; -use solver_types::{ - Address, ConfigSchema, Field, FieldType, Schema, Signature, Transaction, ValidationError, -}; +use serde::Deserialize; +use solver_types::{Address, ConfigSchema, Signature, Transaction, ValidationError}; /// Configuration schema for KMS wallet. pub struct KmsWalletSchema; +/// Dedicated typed configuration for KMS-backed wallets. +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct KmsWalletConfig { + key_id: String, + region: String, + endpoint: Option, +} + +impl KmsWalletConfig { + fn from_json(config: &serde_json::Value) -> Result { + let parsed: Self = serde_json::from_value(config.clone()) + .map_err(|err| ValidationError::DeserializationError(err.to_string()))?; + parsed.validate()?; + Ok(parsed) + } + + fn validate(&self) -> Result<(), ValidationError> { + if self.key_id.trim().is_empty() { + return Err(ValidationError::InvalidValue { + field: "key_id".to_string(), + message: "key_id must be a non-empty string".to_string(), + }); + } + if self.region.trim().is_empty() { + return Err(ValidationError::InvalidValue { + field: "region".to_string(), + message: "region must be a non-empty string".to_string(), + }); + } + Ok(()) + } +} + impl KmsWalletSchema { /// Static validation method for use before instance creation. - pub fn validate_config(config: &toml::Value) -> Result<(), ValidationError> { - let schema = Self; - schema.validate(config) + pub fn validate_config(config: &serde_json::Value) -> Result<(), ValidationError> { + KmsWalletConfig::from_json(config).map(|_| ()) } } impl ConfigSchema for KmsWalletSchema { - fn validate(&self, config: &toml::Value) -> Result<(), ValidationError> { - let schema = Schema::new( - // Required fields - vec![ - Field::new("key_id", FieldType::String).with_validator(|v| match v.as_str() { - Some(s) if !s.is_empty() => Ok(()), - _ => Err("key_id must be a non-empty string".to_string()), - }), - Field::new("region", FieldType::String).with_validator(|v| match v.as_str() { - Some(s) if !s.is_empty() => Ok(()), - _ => Err("region must be a non-empty string".to_string()), - }), - ], - // Optional fields - vec![ - Field::new("endpoint", FieldType::String), // For LocalStack testing - ], - ); - schema.validate(config) + fn validate(&self, config: &serde_json::Value) -> Result<(), ValidationError> { + KmsWalletConfig::from_json(config).map(|_| ()) } } @@ -158,29 +173,11 @@ impl AccountInterface for KmsWallet { /// Factory function to create a KMS wallet from configuration. /// /// Returns an async future that initializes the KMS signer. -pub fn create_account(config: &toml::Value) -> AccountFactoryFuture<'_> { +pub fn create_account(config: &serde_json::Value) -> AccountFactoryFuture<'_> { Box::pin(async move { - KmsWalletSchema::validate_config(config) + let parsed = KmsWalletConfig::from_json(config) .map_err(|e| AccountError::InvalidKey(format!("Invalid configuration: {e}")))?; - - let key_id = config - .get("key_id") - .and_then(|v| v.as_str()) - .ok_or_else(|| AccountError::InvalidKey("key_id required".into()))? - .to_string(); - - let region = config - .get("region") - .and_then(|v| v.as_str()) - .ok_or_else(|| AccountError::InvalidKey("region required".into()))? - .to_string(); - - let endpoint = config - .get("endpoint") - .and_then(|v| v.as_str()) - .map(String::from); - - let wallet = KmsWallet::new(key_id, region, endpoint).await?; + let wallet = KmsWallet::new(parsed.key_id, parsed.region, parsed.endpoint).await?; Ok(Box::new(wallet) as Box) }) diff --git a/crates/solver-account/src/implementations/local.rs b/crates/solver-account/src/implementations/local.rs index 0f9bd427..5491d829 100644 --- a/crates/solver-account/src/implementations/local.rs +++ b/crates/solver-account/src/implementations/local.rs @@ -10,9 +10,9 @@ use alloy_primitives::{Address as AlloyAddress, Bytes, TxKind}; use alloy_signer::Signer; use alloy_signer_local::PrivateKeySigner; use async_trait::async_trait; +use serde::Deserialize; use solver_types::{ - with_0x_prefix, Address, ConfigSchema, Field, FieldType, Schema, SecretString, Signature, - Transaction, + with_0x_prefix, Address, ConfigSchema, SecretString, Signature, Transaction, ValidationError, }; /// Local wallet implementation using Alloy's signer. @@ -26,6 +26,45 @@ pub struct LocalWallet { signer: PrivateKeySigner, } +/// Dedicated typed configuration for local wallet accounts. +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct LocalWalletConfig { + private_key: String, +} + +impl LocalWalletConfig { + fn from_json(config: &serde_json::Value) -> Result { + let parsed: Self = serde_json::from_value(config.clone()) + .map_err(|err| ValidationError::DeserializationError(err.to_string()))?; + parsed.validate()?; + Ok(parsed) + } + + fn validate(&self) -> Result<(), ValidationError> { + let key_without_prefix = self + .private_key + .strip_prefix("0x") + .unwrap_or(self.private_key.as_str()); + + if key_without_prefix.len() != 64 { + return Err(ValidationError::InvalidValue { + field: "private_key".to_string(), + message: "Private key must be 64 hex characters (32 bytes)".to_string(), + }); + } + + if hex::decode(key_without_prefix).is_err() { + return Err(ValidationError::InvalidValue { + field: "private_key".to_string(), + message: "Private key must be valid hexadecimal".to_string(), + }); + } + + Ok(()) + } +} + impl LocalWallet { /// Creates a new LocalWallet from a hex-encoded private key. /// @@ -50,42 +89,14 @@ pub struct LocalWalletSchema; impl LocalWalletSchema { /// Static validation method for use before instance creation - pub fn validate_config(config: &toml::Value) -> Result<(), solver_types::ValidationError> { - let instance = Self; - instance.validate(config) + pub fn validate_config(config: &serde_json::Value) -> Result<(), ValidationError> { + LocalWalletConfig::from_json(config).map(|_| ()) } } impl ConfigSchema for LocalWalletSchema { - fn validate(&self, config: &toml::Value) -> Result<(), solver_types::ValidationError> { - let schema = - Schema::new( - // Required fields - vec![Field::new("private_key", FieldType::String).with_validator( - |value| match value.as_str() { - Some(key) => { - let key_without_prefix = key.strip_prefix("0x").unwrap_or(key); - - if key_without_prefix.len() != 64 { - return Err( - "Private key must be 64 hex characters (32 bytes)".to_string() - ); - } - - if hex::decode(key_without_prefix).is_err() { - return Err("Private key must be valid hexadecimal".to_string()); - } - - Ok(()) - }, - None => Err("Expected string value for private_key".to_string()), - }, - )], - // Optional fields - vec![], - ); - - schema.validate(config) + fn validate(&self, config: &serde_json::Value) -> Result<(), ValidationError> { + LocalWalletConfig::from_json(config).map(|_| ()) } } @@ -169,18 +180,11 @@ impl AccountInterface for LocalWallet { /// Returns an error if: /// - `private_key` is not provided in the configuration /// - The wallet creation fails -pub fn create_account(config: &toml::Value) -> AccountFactoryFuture<'_> { +pub fn create_account(config: &serde_json::Value) -> AccountFactoryFuture<'_> { Box::pin(async move { - // Validate configuration first - LocalWalletSchema::validate_config(config) + let parsed = LocalWalletConfig::from_json(config) .map_err(|e| AccountError::InvalidKey(format!("Invalid configuration: {e}")))?; - - let private_key = config - .get("private_key") - .and_then(|v| v.as_str()) - .ok_or_else(|| AccountError::InvalidKey("private_key required".into()))?; - - Ok(Box::new(LocalWallet::new(private_key)?) as Box) + Ok(Box::new(LocalWallet::new(&parsed.private_key)?) as Box) }) } @@ -214,13 +218,13 @@ mod tests { const INVALID_PRIVATE_KEY: &str = "invalid_key"; const SHORT_PRIVATE_KEY: &str = "1234"; - fn create_test_config(private_key: &str) -> toml::Value { + fn create_test_config(private_key: &str) -> serde_json::Value { let mut config = HashMap::new(); config.insert( "private_key".to_string(), - toml::Value::String(private_key.to_string()), + serde_json::Value::String(private_key.to_string()), ); - toml::Value::Table(config.into_iter().collect()) + serde_json::Value::Object(config.into_iter().collect()) } fn create_test_transaction() -> Transaction { @@ -285,7 +289,24 @@ mod tests { #[test] fn test_schema_validation_missing_private_key() { - let config = toml::Value::Table(HashMap::new().into_iter().collect()); + let config = serde_json::Value::Object(HashMap::new().into_iter().collect()); + let result = LocalWalletSchema::validate_config(&config); + assert!(result.is_err()); + } + + #[test] + fn test_schema_validation_rejects_unknown_fields() { + let config = serde_json::json!({ + "private_key": TEST_PRIVATE_KEY, + "unexpected": "value" + }); + let result = LocalWalletSchema::validate_config(&config); + assert!(result.is_err()); + } + + #[test] + fn test_schema_validation_rejects_non_table_root() { + let config = serde_json::Value::String(TEST_PRIVATE_KEY.to_string()); let result = LocalWalletSchema::validate_config(&config); assert!(result.is_err()); } @@ -353,7 +374,7 @@ mod tests { #[tokio::test] async fn test_create_account_missing_private_key() { - let config = toml::Value::Table(HashMap::new().into_iter().collect()); + let config = serde_json::Value::Object(HashMap::new().into_iter().collect()); let result = create_account(&config).await; assert!(result.is_err()); } diff --git a/crates/solver-account/src/lib.rs b/crates/solver-account/src/lib.rs index cb94eca3..77f37cbe 100644 --- a/crates/solver-account/src/lib.rs +++ b/crates/solver-account/src/lib.rs @@ -103,7 +103,7 @@ pub type AccountFactoryFuture<'a> = /// /// The `for<'a>` higher-ranked trait bound ensures the returned future /// borrows the config safely for its entire lifetime. -pub type AccountFactory = for<'a> fn(&'a toml::Value) -> AccountFactoryFuture<'a>; +pub type AccountFactory = for<'a> fn(&'a serde_json::Value) -> AccountFactoryFuture<'a>; /// Registry trait for account implementations. /// @@ -182,6 +182,13 @@ impl AccountService { #[cfg(test)] mod tests { use super::*; + use serde_json::json; + + fn local_account_config() -> serde_json::Value { + json!({ + "private_key": "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + }) + } #[test] fn test_account_error_display() { @@ -206,10 +213,7 @@ mod tests { async fn test_account_service_new() { use implementations::local::create_account; - let config: toml::Value = toml::from_str( - r#"private_key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80""#, - ) - .unwrap(); + let config = local_account_config(); let account = create_account(&config).await.unwrap(); let service = AccountService::new(account); // Just verify it can be created @@ -220,10 +224,7 @@ mod tests { async fn test_account_service_get_address() { use implementations::local::create_account; - let config: toml::Value = toml::from_str( - r#"private_key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80""#, - ) - .unwrap(); + let config = local_account_config(); let account = create_account(&config).await.unwrap(); let service = AccountService::new(account); @@ -236,10 +237,7 @@ mod tests { use alloy_primitives::U256; use implementations::local::create_account; - let config: toml::Value = toml::from_str( - r#"private_key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80""#, - ) - .unwrap(); + let config = local_account_config(); let account = create_account(&config).await.unwrap(); let service = AccountService::new(account); @@ -264,10 +262,7 @@ mod tests { async fn test_account_service_get_private_key() { use implementations::local::create_account; - let config: toml::Value = toml::from_str( - r#"private_key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80""#, - ) - .unwrap(); + let config = local_account_config(); let account = create_account(&config).await.unwrap(); let service = AccountService::new(account); @@ -282,10 +277,7 @@ mod tests { async fn test_account_service_signer() { use implementations::local::create_account; - let config: toml::Value = toml::from_str( - r#"private_key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80""#, - ) - .unwrap(); + let config = local_account_config(); let account = create_account(&config).await.unwrap(); let service = AccountService::new(account); diff --git a/crates/solver-config/Cargo.toml b/crates/solver-config/Cargo.toml index 4cb6cf35..7f057c1a 100644 --- a/crates/solver-config/Cargo.toml +++ b/crates/solver-config/Cargo.toml @@ -9,10 +9,10 @@ dotenvy = "0.15" regex = "1.10" rust_decimal = { workspace = true } serde = { version = "1.0", features = ["derive"] } +serde_json = { workspace = true } solver-types = { path = "../solver-types" } thiserror = "2.0" tokio = { workspace = true } -toml = { workspace = true } [dev-dependencies] tempfile = "3.8" diff --git a/crates/solver-config/src/lib.rs b/crates/solver-config/src/lib.rs index 53b8b71d..c8ab5bd7 100644 --- a/crates/solver-config/src/lib.rs +++ b/crates/solver-config/src/lib.rs @@ -1,17 +1,10 @@ //! Configuration module for the OIF solver system. //! //! This module provides structures and utilities for managing solver configuration. -//! It supports loading configuration from TOML files and provides validation to ensure +//! It supports loading configuration from JSON files and provides validation to ensure //! all required configuration values are properly set. -//! -//! ## Modular Configuration Support -//! -//! Configurations can be split into multiple files for better organization: -//! - Use `include = ["file1.toml", "file2.toml"]` to include other config files -//! - Each top-level section must be unique across all files (no duplicates allowed) pub mod builders; -mod loader; pub use builders::config::ConfigBuilder; @@ -30,7 +23,7 @@ pub enum ConfigError { /// Error that occurs during file I/O operations. #[error("IO error: {0}")] Io(#[from] std::io::Error), - /// Error that occurs when parsing TOML configuration. + /// Error that occurs when parsing JSON configuration. #[error("Configuration error: {0}")] Parse(String), /// Error that occurs when configuration validation fails. @@ -38,11 +31,9 @@ pub enum ConfigError { Validation(String), } -impl From for ConfigError { - fn from(err: toml::de::Error) -> Self { - // Extract just the message without the huge input dump - let message = err.message().to_string(); - ConfigError::Parse(message) +impl From for ConfigError { + fn from(err: serde_json::Error) -> Self { + ConfigError::Parse(err.to_string()) } } @@ -111,7 +102,7 @@ pub struct StorageConfig { /// Which implementation to use as primary. pub primary: String, /// Map of storage implementation names to their configurations. - pub implementations: HashMap, + pub implementations: HashMap, /// Interval in seconds for cleaning up expired storage entries. pub cleanup_interval_seconds: u64, } @@ -120,8 +111,8 @@ pub struct StorageConfig { #[derive(Debug, Clone, Deserialize, Serialize)] pub struct DeliveryConfig { /// Map of delivery implementation names to their configurations. - /// Each implementation has its own configuration format stored as raw TOML values. - pub implementations: HashMap, + /// Each implementation has its own configuration format stored as raw JSON values. + pub implementations: HashMap, /// Minimum number of confirmations required for transactions. /// Defaults to 3 confirmations if not specified. #[serde(default = "default_confirmations")] @@ -152,15 +143,15 @@ pub struct AccountConfig { /// Which implementation to use as primary. pub primary: String, /// Map of account implementation names to their configurations. - pub implementations: HashMap, + pub implementations: HashMap, } /// Configuration for order discovery. #[derive(Debug, Clone, Deserialize, Serialize)] pub struct DiscoveryConfig { /// Map of discovery implementation names to their configurations. - /// Each implementation has its own configuration format stored as raw TOML values. - pub implementations: HashMap, + /// Each implementation has its own configuration format stored as raw JSON values. + pub implementations: HashMap, } /// Configuration for order processing. @@ -168,7 +159,7 @@ pub struct DiscoveryConfig { pub struct OrderConfig { /// Map of order implementation names to their configurations. /// Each implementation handles specific order types. - pub implementations: HashMap, + pub implementations: HashMap, /// Strategy configuration for order execution. pub strategy: StrategyConfig, /// Whitelisted callback contract addresses in EIP-7930 InteropAddress format. @@ -187,7 +178,7 @@ pub struct StrategyConfig { /// Which strategy implementation to use as primary. pub primary: String, /// Map of strategy implementation names to their configurations. - pub implementations: HashMap, + pub implementations: HashMap, } /// Configuration for settlement operations. @@ -195,7 +186,7 @@ pub struct StrategyConfig { pub struct SettlementConfig { /// Map of settlement implementation names to their configurations. /// Each implementation handles specific settlement mechanisms. - pub implementations: HashMap, + pub implementations: HashMap, /// Poll interval in seconds for settlement readiness monitoring. /// Defaults to 3 seconds if not specified. #[serde(default = "default_settlement_poll_interval_seconds")] @@ -292,7 +283,7 @@ pub struct PricingConfig { #[serde(default)] pub fallbacks: Vec, /// Map of pricing implementation names to their configurations. - pub implementations: HashMap, + pub implementations: HashMap, } fn default_gas_buffer_bps() -> u32 { @@ -452,24 +443,12 @@ pub(crate) fn resolve_env_vars(input: &str) -> Result { impl Config { /// Loads configuration from a file with async environment variable resolution. - /// - /// This method supports modular configuration through include directives: - /// - `include = ["file1.toml", "file2.toml"]` - Include specific files - /// - /// Each top-level section must be unique across all configuration files. - /// - /// Environment variables are loaded from .env files in the current working directory. pub async fn from_file(path: &str) -> Result { - let path_buf = Path::new(path); - let base_dir = path_buf.parent().unwrap_or_else(|| Path::new(".")); - - // Create loader with config file's base directory for includes - let mut loader = loader::ConfigLoader::new(base_dir); - - let file_name = path_buf - .file_name() - .ok_or_else(|| ConfigError::Validation(format!("Invalid path: {path}")))?; - loader.load_config(file_name).await + let content = tokio::fs::read_to_string(Path::new(path)).await?; + let resolved = resolve_env_vars(&content)?; + let config: Config = serde_json::from_str(&resolved)?; + config.validate()?; + Ok(config) } /// Validates the configuration to ensure all required fields are properly set. @@ -682,7 +661,7 @@ impl Config { // Check for duplicate coverage for network_value in network_ids { - let network_id = network_value.as_integer().ok_or_else(|| { + let network_id = network_value.as_i64().ok_or_else(|| { ConfigError::Validation(format!( "Invalid network_id in settlement '{impl_name}'" )) @@ -724,7 +703,7 @@ impl Config { /// Implementation of FromStr trait for Config to enable parsing from string. /// -/// This allows configuration to be parsed from TOML strings using the standard +/// This allows configuration to be parsed from JSON strings using the standard /// string parsing interface. Environment variables are resolved and the /// configuration is automatically validated after parsing. impl FromStr for Config { @@ -732,7 +711,7 @@ impl FromStr for Config { fn from_str(s: &str) -> Result { let resolved = resolve_env_vars(s)?; - let config: Config = toml::from_str(&resolved)?; + let config: Config = serde_json::from_str(&resolved)?; config.validate()?; Ok(config) } @@ -741,6 +720,79 @@ impl FromStr for Config { #[cfg(test)] mod tests { use super::*; + use serde_json::json; + + fn parse_json_fixture(value: serde_json::Value) -> Result { + let json_string = + serde_json::to_string(&value).map_err(|err| ConfigError::Parse(err.to_string()))?; + Config::from_str(&json_string) + } + + fn test_network(chain_id: u32, rpc_url: &str) -> serde_json::Value { + json!({ + "chain_id": chain_id, + "input_settler_address": "0x1234567890123456789012345678901234567890", + "output_settler_address": "0x0987654321098765432109876543210987654321", + "rpc_urls": [{ "http": rpc_url }], + "tokens": [{ + "address": "0xabcdef1234567890abcdef1234567890abcdef12", + "symbol": "TEST", + "decimals": 18 + }] + }) + } + + fn base_config_json( + networks: serde_json::Value, + order_implementations: serde_json::Value, + settlement_implementations: serde_json::Value, + ) -> serde_json::Value { + json!({ + "solver": { + "id": "test", + "monitoring_timeout_seconds": 300, + "min_profitability_pct": 5.0 + }, + "networks": networks, + "storage": { + "primary": "memory", + "cleanup_interval_seconds": 3600, + "implementations": { + "memory": {} + } + }, + "delivery": { + "implementations": { + "test": {} + } + }, + "account": { + "primary": "local", + "implementations": { + "local": { + "private_key": "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + } + } + }, + "discovery": { + "implementations": { + "test": {} + } + }, + "order": { + "implementations": order_implementations, + "strategy": { + "primary": "simple", + "implementations": { + "simple": {} + } + } + }, + "settlement": { + "implementations": settlement_implementations + } + }) + } #[test] fn test_env_var_resolution() { @@ -748,9 +800,9 @@ mod tests { std::env::set_var("TEST_HOST", "localhost"); std::env::set_var("TEST_PORT", "5432"); - let input = "host = \"${TEST_HOST}:${TEST_PORT}\""; + let input = r#"{"host":"${TEST_HOST}:${TEST_PORT}"}"#; let result = resolve_env_vars(input).unwrap(); - assert_eq!(result, "host = \"localhost:5432\""); + assert_eq!(result, r#"{"host":"localhost:5432"}"#); // Clean up std::env::remove_var("TEST_HOST"); @@ -759,14 +811,14 @@ mod tests { #[test] fn test_env_var_with_default() { - let input = "value = \"${MISSING_VAR:-default_value}\""; + let input = r#"{"value":"${MISSING_VAR:-default_value}"}"#; let result = resolve_env_vars(input).unwrap(); - assert_eq!(result, "value = \"default_value\""); + assert_eq!(result, r#"{"value":"default_value"}"#); } #[test] fn test_missing_env_var_error() { - let input = "value = \"${MISSING_VAR}\""; + let input = r#"{"value":"${MISSING_VAR}"}"#; let result = resolve_env_vars(input); assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("MISSING_VAR")); @@ -776,62 +828,62 @@ mod tests { fn test_config_with_env_vars() { // Set environment variable std::env::set_var("TEST_SOLVER_ID", "test-solver"); - - let config_str = r#" -[solver] -id = "${TEST_SOLVER_ID}" -monitoring_timeout_minutes = 5 -min_profitability_pct = 1.0 - -[networks.1] -input_settler_address = "0x1234567890123456789012345678901234567890" -output_settler_address = "0x0987654321098765432109876543210987654321" -[[networks.1.rpc_urls]] -http = "http://localhost:8545" -[[networks.1.tokens]] -address = "0xabcdef1234567890abcdef1234567890abcdef12" -symbol = "TEST" -decimals = 18 - -[networks.2] -input_settler_address = "0x1234567890123456789012345678901234567890" -output_settler_address = "0x0987654321098765432109876543210987654321" -[[networks.2.rpc_urls]] -http = "http://localhost:8546" -[[networks.2.tokens]] -address = "0xabcdef1234567890abcdef1234567890abcdef12" -symbol = "TEST" -decimals = 18 - -[storage] -primary = "memory" -cleanup_interval_seconds = 3600 -[storage.implementations.memory] - -[delivery] -[delivery.implementations.test] - -[account] -primary = "local" -[account.implementations.local] -private_key = "${TEST_PRIVATE_KEY:-0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80}" - -[discovery] -[discovery.implementations.test] - -[order] -[order.implementations.test] -[order.strategy] -primary = "simple" -[order.strategy.implementations.simple] - -[settlement] -[settlement.implementations.test] -order = "test" -network_ids = [1, 2] -"#; - - let config: Config = config_str.parse().unwrap(); + let config = parse_json_fixture(json!({ + "solver": { + "id": "${TEST_SOLVER_ID}", + "monitoring_timeout_seconds": 300, + "min_profitability_pct": 1.0 + }, + "networks": { + "1": test_network(1, "http://localhost:8545"), + "2": test_network(2, "http://localhost:8546") + }, + "storage": { + "primary": "memory", + "cleanup_interval_seconds": 3600, + "implementations": { + "memory": {} + } + }, + "delivery": { + "implementations": { + "test": {} + } + }, + "account": { + "primary": "local", + "implementations": { + "local": { + "private_key": "${TEST_PRIVATE_KEY:-0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80}" + } + } + }, + "discovery": { + "implementations": { + "test": {} + } + }, + "order": { + "implementations": { + "test": {} + }, + "strategy": { + "primary": "simple", + "implementations": { + "simple": {} + } + } + }, + "settlement": { + "implementations": { + "test": { + "order": "test", + "network_ids": [1, 2] + } + } + } + })) + .unwrap(); assert_eq!(config.solver.id, "test-solver"); assert_eq!( config.solver.min_profitability_pct, @@ -844,129 +896,57 @@ network_ids = [1, 2] #[test] fn test_config_allows_empty_network_tokens() { - let config_str = r#" -[solver] -id = "test-empty-tokens" -monitoring_timeout_minutes = 5 -min_profitability_pct = 1.0 - -[networks.1] -input_settler_address = "0x1234567890123456789012345678901234567890" -output_settler_address = "0x0987654321098765432109876543210987654321" -tokens = [] -[[networks.1.rpc_urls]] -http = "http://localhost:8545" - -[networks.2] -input_settler_address = "0x1234567890123456789012345678901234567890" -output_settler_address = "0x0987654321098765432109876543210987654321" -tokens = [] -[[networks.2.rpc_urls]] -http = "http://localhost:8546" - -[storage] -primary = "memory" -cleanup_interval_seconds = 3600 -[storage.implementations.memory] - -[delivery] -[delivery.implementations.test] - -[account] -primary = "local" -[account.implementations.local] -private_key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" - -[discovery] -[discovery.implementations.test] - -[order] -[order.implementations.test] -[order.strategy] -primary = "simple" -[order.strategy.implementations.simple] - -[settlement] -[settlement.implementations.test] -order = "test" -network_ids = [1, 2] -"#; - - let config: Config = config_str.parse().expect("Config should parse"); + fn empty_token_network(rpc_url: &str) -> serde_json::Value { + json!({ + "input_settler_address": "0x1234567890123456789012345678901234567890", + "output_settler_address": "0x0987654321098765432109876543210987654321", + "rpc_urls": [{ "http": rpc_url }], + "tokens": [] + }) + } + + let config = parse_json_fixture(base_config_json( + json!({ + "1": empty_token_network("http://localhost:8545"), + "2": empty_token_network("http://localhost:8546") + }), + json!({ + "test": {} + }), + json!({ + "test": { + "order": "test", + "network_ids": [1, 2] + } + }), + )) + .expect("Config should parse"); assert_eq!(config.networks.get(&1).unwrap().tokens.len(), 0); assert_eq!(config.networks.get(&2).unwrap().tokens.len(), 0); } #[test] fn test_duplicate_settlement_coverage_rejected() { - let config_str = r#" -[solver] -id = "test" -monitoring_timeout_minutes = 5 -min_profitability_pct = 5.0 # Minimum profitability percentage required to execute orders - -[networks.1] -input_settler_address = "0x1234567890123456789012345678901234567890" -output_settler_address = "0x0987654321098765432109876543210987654321" -[[networks.1.rpc_urls]] -http = "http://localhost:8545" -[[networks.1.tokens]] -address = "0xabcdef1234567890abcdef1234567890abcdef12" -symbol = "TEST" -decimals = 18 - -[networks.2] -input_settler_address = "0x1234567890123456789012345678901234567890" -output_settler_address = "0x0987654321098765432109876543210987654321" -[[networks.2.rpc_urls]] -http = "http://localhost:8546" -[[networks.2.tokens]] -address = "0xabcdef1234567890abcdef1234567890abcdef12" -symbol = "TEST" -decimals = 18 - -[networks.3] -input_settler_address = "0x1234567890123456789012345678901234567890" -output_settler_address = "0x0987654321098765432109876543210987654321" -[[networks.3.rpc_urls]] -http = "http://localhost:8547" -[[networks.3.tokens]] -address = "0xabcdef1234567890abcdef1234567890abcdef12" -symbol = "TEST" -decimals = 18 - -[storage] -primary = "memory" -cleanup_interval_seconds = 3600 -[storage.implementations.memory] - -[delivery] -[delivery.implementations.test] - -[account] -primary = "local" -[account.implementations.local] -private_key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" - -[discovery] -[discovery.implementations.test] - -[order] -[order.implementations.eip7683] -[order.strategy] -primary = "simple" -[order.strategy.implementations.simple] - -[settlement.implementations.impl1] -order = "eip7683" -network_ids = [1, 2] - -[settlement.implementations.impl2] -order = "eip7683" -network_ids = [2, 3] # Network 2 overlaps with impl1 -"#; - - let result = Config::from_str(config_str); + let result = parse_json_fixture(base_config_json( + json!({ + "1": test_network(1, "http://localhost:8545"), + "2": test_network(2, "http://localhost:8546"), + "3": test_network(3, "http://localhost:8547") + }), + json!({ + "eip7683": {} + }), + json!({ + "impl1": { + "order": "eip7683", + "network_ids": [1, 2] + }, + "impl2": { + "order": "eip7683", + "network_ids": [2, 3] + } + }), + )); assert!(result.is_err()); let err = result.unwrap_err(); // The test should fail because network 2 is covered by both impl1 and impl2 @@ -982,60 +962,20 @@ network_ids = [2, 3] # Network 2 overlaps with impl1 #[test] fn test_missing_settlement_standard_rejected() { - let config_str = r#" -[solver] -id = "test" -monitoring_timeout_minutes = 5 -min_profitability_pct = 5.0 # Minimum profitability percentage required to execute orders - -[networks.1] -input_settler_address = "0x1234567890123456789012345678901234567890" -output_settler_address = "0x0987654321098765432109876543210987654321" -[[networks.1.rpc_urls]] -http = "http://localhost:8545" -[[networks.1.tokens]] -address = "0xabcdef1234567890abcdef1234567890abcdef12" -symbol = "TEST" -decimals = 18 - -[networks.2] -input_settler_address = "0x1234567890123456789012345678901234567890" -output_settler_address = "0x0987654321098765432109876543210987654321" -[[networks.2.rpc_urls]] -http = "http://localhost:8546" -[[networks.2.tokens]] -address = "0xabcdef1234567890abcdef1234567890abcdef12" -symbol = "TEST" -decimals = 18 - -[storage] -primary = "memory" -cleanup_interval_seconds = 3600 -[storage.implementations.memory] - -[delivery] -[delivery.implementations.test] - -[account] -primary = "local" -[account.implementations.local] -private_key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" - -[discovery] -[discovery.implementations.test] - -[order] -[order.implementations.eip7683] -[order.strategy] -primary = "simple" -[order.strategy.implementations.simple] - -[settlement.implementations.impl1] -# Missing 'standard' field -network_ids = [1, 2] -"#; - - let result = Config::from_str(config_str); + let result = parse_json_fixture(base_config_json( + json!({ + "1": test_network(1, "http://localhost:8545"), + "2": test_network(2, "http://localhost:8546") + }), + json!({ + "eip7683": {} + }), + json!({ + "impl1": { + "network_ids": [1, 2] + } + }), + )); assert!(result.is_err()); let err = result.unwrap_err(); assert!(err.to_string().contains("missing 'order' field")); @@ -1043,60 +983,21 @@ network_ids = [1, 2] #[test] fn test_settlement_references_invalid_network() { - let config_str = r#" -[solver] -id = "test" -monitoring_timeout_minutes = 5 -min_profitability_pct = 5.0 # Minimum profitability percentage required to execute orders - -[networks.1] -input_settler_address = "0x1234567890123456789012345678901234567890" -output_settler_address = "0x0987654321098765432109876543210987654321" -[[networks.1.rpc_urls]] -http = "http://localhost:8545" -[[networks.1.tokens]] -address = "0xabcdef1234567890abcdef1234567890abcdef12" -symbol = "TEST" -decimals = 18 - -[networks.2] -input_settler_address = "0x1234567890123456789012345678901234567890" -output_settler_address = "0x0987654321098765432109876543210987654321" -[[networks.2.rpc_urls]] -http = "http://localhost:8546" -[[networks.2.tokens]] -address = "0xabcdef1234567890abcdef1234567890abcdef12" -symbol = "TEST" -decimals = 18 - -[storage] -primary = "memory" -cleanup_interval_seconds = 3600 -[storage.implementations.memory] - -[delivery] -[delivery.implementations.test] - -[account] -primary = "local" -[account.implementations.local] -private_key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" - -[discovery] -[discovery.implementations.test] - -[order] -[order.implementations.eip7683] -[order.strategy] -primary = "simple" -[order.strategy.implementations.simple] - -[settlement.implementations.impl1] -order = "eip7683" -network_ids = [1, 2, 999] # Network 999 doesn't exist -"#; - - let result = Config::from_str(config_str); + let result = parse_json_fixture(base_config_json( + json!({ + "1": test_network(1, "http://localhost:8545"), + "2": test_network(2, "http://localhost:8546") + }), + json!({ + "eip7683": {} + }), + json!({ + "impl1": { + "order": "eip7683", + "network_ids": [1, 2, 999] + } + }), + )); assert!(result.is_err()); let err = result.unwrap_err(); assert!(err @@ -1106,61 +1007,22 @@ network_ids = [1, 2, 999] # Network 999 doesn't exist #[test] fn test_order_standard_without_settlement() { - let config_str = r#" -[solver] -id = "test" -monitoring_timeout_minutes = 5 -min_profitability_pct = 5.0 # Minimum profitability percentage required to execute orders - -[networks.1] -input_settler_address = "0x1234567890123456789012345678901234567890" -output_settler_address = "0x0987654321098765432109876543210987654321" -[[networks.1.rpc_urls]] -http = "http://localhost:8545" -[[networks.1.tokens]] -address = "0xabcdef1234567890abcdef1234567890abcdef12" -symbol = "TEST" -decimals = 18 - -[networks.2] -input_settler_address = "0x1234567890123456789012345678901234567890" -output_settler_address = "0x0987654321098765432109876543210987654321" -[[networks.2.rpc_urls]] -http = "http://localhost:8546" -[[networks.2.tokens]] -address = "0xabcdef1234567890abcdef1234567890abcdef12" -symbol = "TEST" -decimals = 18 - -[storage] -primary = "memory" -cleanup_interval_seconds = 3600 -[storage.implementations.memory] - -[delivery] -[delivery.implementations.test] - -[account] -primary = "local" -[account.implementations.local] -private_key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" - -[discovery] -[discovery.implementations.test] - -[order] -[order.implementations.eip7683] -[order.implementations.eip9999] # Order standard with no settlement -[order.strategy] -primary = "simple" -[order.strategy.implementations.simple] - -[settlement.implementations.impl1] -order = "eip7683" # Only covers eip7683, not eip9999 -network_ids = [1, 2] -"#; - - let result = Config::from_str(config_str); + let result = parse_json_fixture(base_config_json( + json!({ + "1": test_network(1, "http://localhost:8545"), + "2": test_network(2, "http://localhost:8546") + }), + json!({ + "eip7683": {}, + "eip9999": {} + }), + json!({ + "impl1": { + "order": "eip7683", + "network_ids": [1, 2] + } + }), + )); assert!(result.is_err()); let err = result.unwrap_err(); assert!(err diff --git a/crates/solver-config/src/loader.rs b/crates/solver-config/src/loader.rs deleted file mode 100644 index 0af5b278..00000000 --- a/crates/solver-config/src/loader.rs +++ /dev/null @@ -1,427 +0,0 @@ -//! Configuration loader module for handling modular configuration files. -//! -//! This module provides functionality to load configuration from multiple files -//! and validate that sections are unique across files to prevent merge conflicts. - -use crate::{resolve_env_vars, Config, ConfigError}; -use std::collections::{HashMap, HashSet}; -use std::path::{Path, PathBuf}; - -/// Configuration loader that handles multi-file configurations with includes. -pub struct ConfigLoader { - /// Base path for resolving relative includes - base_path: PathBuf, - /// Track loaded files to prevent circular includes - loaded_files: HashSet, - /// Track which sections come from which files for error reporting - section_sources: HashMap, -} - -impl ConfigLoader { - /// Creates a new ConfigLoader with the given base path. - pub fn new(base_path: impl AsRef) -> Self { - Self { - base_path: base_path.as_ref().to_path_buf(), - loaded_files: HashSet::new(), - section_sources: HashMap::new(), - } - } - - /// Loads environment variables from .env files in the current working directory. - /// - /// This method attempts to load .env files in the following order: - /// 1. .env.local (highest priority) - /// 2. .env - /// - /// Later files override earlier ones. - fn load_env_files_from_cwd(&self) -> Result<(), ConfigError> { - let env_files = [".env", ".env.local"]; - - for env_file in &env_files { - let env_path = PathBuf::from(env_file); - if env_path.exists() { - dotenvy::from_path(&env_path).map_err(|e| { - ConfigError::Validation(format!( - "Failed to load environment file {}: {}", - env_path.display(), - e - )) - })?; - } - } - - Ok(()) - } - - /// Loads a configuration file and all its includes, loading .env files first. - pub async fn load_config( - &mut self, - config_path: impl AsRef, - ) -> Result { - // Load environment variables first - self.load_env_files_from_cwd()?; - - // Then load the config - self.load_config_without_env(config_path).await - } - - /// Loads a configuration file and all its includes without loading .env files. - /// Assumes environment variables have already been loaded. - async fn load_config_without_env( - &mut self, - config_path: impl AsRef, - ) -> Result { - let config_path = self.resolve_path(config_path)?; - - // Load the main configuration file - let main_content = self.load_file(&config_path).await?; - let main_toml: toml::Value = toml::from_str(&main_content)?; - - // Check for includes - let includes = self.extract_includes(&main_toml)?; - - // If no includes, just parse and return the main config - if includes.is_empty() { - let config: Config = main_content.parse()?; - return Ok(config); - } - - // Build combined TOML with validation - let combined_toml = self - .load_and_combine(main_toml, includes, config_path.clone()) - .await?; - - // Convert to Config and validate - let config_str = toml::to_string(&combined_toml) - .map_err(|e| ConfigError::Parse(format!("Failed to serialize combined config: {e}")))?; - let config: Config = config_str.parse()?; - - Ok(config) - } - - /// Loads a file and resolves environment variables. - async fn load_file(&mut self, path: &Path) -> Result { - // Check for circular includes - let canonical_path = path.canonicalize().map_err(|e| { - ConfigError::Io(std::io::Error::new( - std::io::ErrorKind::NotFound, - format!("Cannot resolve path {}: {}", path.display(), e), - )) - })?; - - if !self.loaded_files.insert(canonical_path.clone()) { - return Err(ConfigError::Validation(format!( - "Circular include detected: {} was already loaded", - canonical_path.display() - ))); - } - - let content = std::fs::read_to_string(path)?; - resolve_env_vars(&content) - } - - /// Extracts include directives from the configuration. - fn extract_includes(&self, toml: &toml::Value) -> Result, ConfigError> { - let mut includes = Vec::new(); - - // Check for include array - if let Some(include_value) = toml.get("include") { - if let Some(include_array) = include_value.as_array() { - for item in include_array { - if let Some(path_str) = item.as_str() { - includes.push(PathBuf::from(path_str)); - } else { - return Err(ConfigError::Validation( - "Include array must contain only strings".into(), - )); - } - } - } else if let Some(path_str) = include_value.as_str() { - includes.push(PathBuf::from(path_str)); - } else { - return Err(ConfigError::Validation( - "Include must be a string or array of strings".into(), - )); - } - } - - Ok(includes) - } - - /// Loads and combines configuration files with section uniqueness validation. - async fn load_and_combine( - &mut self, - mut main_toml: toml::Value, - includes: Vec, - main_file_path: PathBuf, - ) -> Result { - // Remove include directives from main config - if let Some(table) = main_toml.as_table_mut() { - table.remove("include"); - } - - // Track sections in main file - if let Some(main_table) = main_toml.as_table() { - for key in main_table.keys() { - self.section_sources - .insert(key.clone(), main_file_path.clone()); - } - } - - // Load and validate each included file - for include_path in includes { - let resolved_path = self.resolve_path(&include_path)?; - let include_content = self.load_file(&resolved_path).await?; - let include_toml: toml::Value = toml::from_str(&include_content)?; - - // Validate no duplicate sections - if let Some(include_table) = include_toml.as_table() { - for key in include_table.keys() { - if let Some(existing_source) = self.section_sources.get(key) { - return Err(ConfigError::Validation(format!( - "Duplicate section '{}' found in {} and {}. \ - Each top-level section must be unique across all configuration files.", - key, - existing_source.display(), - resolved_path.display() - ))); - } - self.section_sources - .insert(key.clone(), resolved_path.clone()); - } - - // Merge the tables - if let Some(main_table) = main_toml.as_table_mut() { - for (key, value) in include_table { - main_table.insert(key.clone(), value.clone()); - } - } - } - } - - Ok(main_toml) - } - - /// Resolves a path relative to the base path. - fn resolve_path(&self, path: impl AsRef) -> Result { - let path = path.as_ref(); - - let resolved = if path.is_absolute() { - path.to_path_buf() - } else { - self.base_path.join(path) - }; - - // Verify the file exists - if !resolved.exists() { - return Err(ConfigError::Io(std::io::Error::new( - std::io::ErrorKind::NotFound, - format!("Configuration file not found: {}", resolved.display()), - ))); - } - - Ok(resolved) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::fs; - use tempfile::TempDir; - - #[tokio::test] - async fn test_single_file_config() { - let temp_dir = TempDir::new().unwrap(); - let config_path = temp_dir.path().join("config.toml"); - - let config_content = r#" -[solver] -id = "test-solver" -monitoring_timeout_minutes = 5 -min_profitability_pct = 1.0 - -[networks.1] -input_settler_address = "0x1234567890123456789012345678901234567890" -output_settler_address = "0x0987654321098765432109876543210987654321" -[[networks.1.rpc_urls]] -http = "http://localhost:8545" -[[networks.1.tokens]] -address = "0xabcdef1234567890abcdef1234567890abcdef12" -symbol = "TEST" -decimals = 18 - -[networks.2] -input_settler_address = "0x1234567890123456789012345678901234567890" -output_settler_address = "0x0987654321098765432109876543210987654321" -[[networks.2.rpc_urls]] -http = "http://localhost:8546" -[[networks.2.tokens]] -address = "0xabcdef1234567890abcdef1234567890abcdef12" -symbol = "TEST" -decimals = 18 - -[storage] -primary = "memory" -cleanup_interval_seconds = 3600 -[storage.implementations.memory] - -[delivery] -[delivery.implementations.test] - -[account] -primary = "local" -[account.implementations.local] -private_key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" - -[discovery] -[discovery.implementations.test] - -[order] -[order.implementations.test] -[order.strategy] -primary = "simple" -[order.strategy.implementations.simple] - -[settlement] -[settlement.implementations.test] -order = "test" -network_ids = [1, 2] -"#; - - fs::write(&config_path, config_content).unwrap(); - - let mut loader = ConfigLoader::new(temp_dir.path()); - let config = loader.load_config(&config_path).await.unwrap(); - - assert_eq!(config.solver.id, "test-solver"); - } - - #[tokio::test] - async fn test_config_with_includes() { - let temp_dir = TempDir::new().unwrap(); - - // Main config - let main_config = r#" -include = ["networks.toml", "storage.toml"] -[solver] -id = "test-solver" -monitoring_timeout_minutes = 5 -min_profitability_pct = 1.0 -"#; - - // Networks config - let networks_config = r#" -[networks.1] -input_settler_address = "0x1234567890123456789012345678901234567890" -output_settler_address = "0x0987654321098765432109876543210987654321" -[[networks.1.rpc_urls]] -http = "http://localhost:8545" -[[networks.1.tokens]] -address = "0xabcdef1234567890abcdef1234567890abcdef12" -symbol = "TEST" -decimals = 18 - -[networks.2] -input_settler_address = "0x1234567890123456789012345678901234567890" -output_settler_address = "0x0987654321098765432109876543210987654321" -[[networks.2.rpc_urls]] -http = "http://localhost:8546" -[[networks.2.tokens]] -address = "0xabcdef1234567890abcdef1234567890abcdef12" -symbol = "TEST" -decimals = 18 -"#; - - // Storage config - let storage_config = r#" -[storage] -primary = "memory" -cleanup_interval_seconds = 3600 -[storage.implementations.memory] - -[delivery] -[delivery.implementations.test] - -[account] -primary = "local" -[account.implementations.local] -private_key = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" - -[discovery] -[discovery.implementations.test] - -[order] -[order.implementations.test] -[order.strategy] -primary = "simple" -[order.strategy.implementations.simple] - -[settlement] -[settlement.implementations.test] -order = "test" -network_ids = [1, 2] -"#; - - fs::write(temp_dir.path().join("main.toml"), main_config).unwrap(); - fs::write(temp_dir.path().join("networks.toml"), networks_config).unwrap(); - fs::write(temp_dir.path().join("storage.toml"), storage_config).unwrap(); - - let mut loader = ConfigLoader::new(temp_dir.path()); - let config = loader.load_config("main.toml").await.unwrap(); - - assert_eq!(config.solver.id, "test-solver"); - assert_eq!(config.storage.primary, "memory"); - } - - #[tokio::test] - async fn test_duplicate_section_error() { - let temp_dir = TempDir::new().unwrap(); - - // Main config with solver section - let main_config = r#" -include = ["duplicate.toml"] - -[solver] -id = "test-solver" -"#; - - // Include with duplicate solver section (should cause error) - let duplicate_config = r#" -[solver] -id = "another-solver" -"#; - - fs::write(temp_dir.path().join("main.toml"), main_config).unwrap(); - fs::write(temp_dir.path().join("duplicate.toml"), duplicate_config).unwrap(); - - let mut loader = ConfigLoader::new(temp_dir.path()); - let result = loader.load_config("main.toml").await; - - assert!(result.is_err()); - let error_msg = result.unwrap_err().to_string(); - assert!(error_msg.contains("Duplicate section 'solver'")); - } - - #[tokio::test] - async fn test_self_include_detection() { - let temp_dir = TempDir::new().unwrap(); - - // Create a config that includes itself - let config = r#" -include = ["self.toml"] - -[solver] -id = "test-solver" -"#; - - fs::write(temp_dir.path().join("self.toml"), config).unwrap(); - - let mut loader = ConfigLoader::new(temp_dir.path()); - let result = loader.load_config("self.toml").await; - - assert!(result.is_err()); - let error_msg = result.unwrap_err().to_string(); - assert!(error_msg.contains("already loaded")); - } -} diff --git a/crates/solver-core/Cargo.toml b/crates/solver-core/Cargo.toml index 1fbc7c6d..54330ebb 100644 --- a/crates/solver-core/Cargo.toml +++ b/crates/solver-core/Cargo.toml @@ -23,7 +23,6 @@ solver-storage = { path = "../solver-storage" } solver-types = { path = "../solver-types" } thiserror = "2.0" tokio = { version = "1.0", features = ["full"] } -toml = { workspace = true } tracing = "0.1" [dev-dependencies] diff --git a/crates/solver-core/src/builder/mod.rs b/crates/solver-core/src/builder/mod.rs index 0d50da5d..540961b3 100644 --- a/crates/solver-core/src/builder/mod.rs +++ b/crates/solver-core/src/builder/mod.rs @@ -77,36 +77,36 @@ impl SolverBuilder { factories: SolverFactories, ) -> Result where - SF: Fn(&toml::Value) -> Result, StorageError>, + SF: Fn(&serde_json::Value) -> Result, StorageError>, for<'a> AF: Fn( - &'a toml::Value, + &'a serde_json::Value, ) -> Pin< Box, AccountError>> + Send + 'a>, >, DF: Fn( - &toml::Value, + &serde_json::Value, &solver_types::NetworksConfig, &solver_account::AccountSigner, &std::collections::HashMap, ) -> Result, DeliveryError>, DIF: Fn( - &toml::Value, + &serde_json::Value, &solver_types::NetworksConfig, ) -> Result, DiscoveryError>, OF: Fn( - &toml::Value, + &serde_json::Value, &solver_types::NetworksConfig, &solver_types::oracle::OracleRoutes, ) -> Result, OrderError>, PF: Fn( - &toml::Value, + &serde_json::Value, ) -> Result, solver_types::PricingError>, SEF: Fn( - &toml::Value, + &serde_json::Value, &solver_types::NetworksConfig, Arc, ) -> Result, SettlementError>, - STF: Fn(&toml::Value) -> Result, StrategyError>, + STF: Fn(&serde_json::Value) -> Result, StrategyError>, { // Create storage implementations let mut storage_impls = HashMap::new(); @@ -230,7 +230,7 @@ impl SolverBuilder { if let Some(factory) = factories.delivery_factories.get(name) { // Parse per-network account mappings from config let mut network_signers = HashMap::new(); - if let Some(accounts_table) = config.get("accounts").and_then(|v| v.as_table()) { + if let Some(accounts_table) = config.get("accounts").and_then(|v| v.as_object()) { for (network_id_str, account_name_value) in accounts_table { if let Ok(network_id) = network_id_str.parse::() { if let Some(account_name) = account_name_value.as_str() { @@ -262,7 +262,7 @@ impl SolverBuilder { let implementation_arc: Arc = implementation.into(); for network_id_value in network_ids { - if let Some(network_id) = network_id_value.as_integer() { + if let Some(network_id) = network_id_value.as_i64() { let network_id = network_id as u64; delivery_implementations .insert(network_id, implementation_arc.clone()); diff --git a/crates/solver-core/src/engine/mod.rs b/crates/solver-core/src/engine/mod.rs index 7b87f15b..7f109654 100644 --- a/crates/solver-core/src/engine/mod.rs +++ b/crates/solver-core/src/engine/mod.rs @@ -642,7 +642,7 @@ mod tests { use super::*; use crate::engine::event_bus::EventBus; use solver_account::AccountService; - use solver_config::Config; + use solver_config::{Config, ConfigBuilder}; use solver_delivery::DeliveryService; use solver_discovery::DiscoveryService; use solver_order::OrderService; @@ -668,62 +668,7 @@ mod tests { EventBus, Arc, ) { - // Create minimal config for testing - let config_toml = r#" - [solver] - id = "test-solver" - monitoring_timeout_seconds = 30 - min_profitability_pct = 1.0 - - [storage] - primary = "memory" - cleanup_interval_seconds = 3600 - [storage.implementations.memory] - - [delivery] - min_confirmations = 1 - [delivery.implementations] - - [account] - primary = "local" - [account.implementations.local] - private_key = "0x1234567890123456789012345678901234567890123456789012345678901234" - - [discovery] - [discovery.implementations] - - [order] - [order.implementations] - [order.strategy] - primary = "simple" - [order.strategy.implementations.simple] - - [settlement] - [settlement.implementations] - - [networks.1] - chain_id = 1 - input_settler_address = "0x1111111111111111111111111111111111111111" - output_settler_address = "0x2222222222222222222222222222222222222222" - [[networks.1.rpc_urls]] - http = "http://localhost:8545" - [[networks.1.tokens]] - symbol = "TEST" - address = "0x3333333333333333333333333333333333333333" - decimals = 18 - - [networks.2] - chain_id = 2 - input_settler_address = "0x4444444444444444444444444444444444444444" - output_settler_address = "0x5555555555555555555555555555555555555555" - [[networks.2.rpc_urls]] - http = "http://localhost:8546" - [[networks.2.tokens]] - symbol = "TEST2" - address = "0x6666666666666666666666666666666666666666" - decimals = 18 - "#; - let config: Config = toml::from_str(config_toml).expect("Failed to parse test config"); + let config: Config = ConfigBuilder::new().build(); // Create mock services using proper constructors let storage = Arc::new(StorageService::new(Box::new( @@ -731,10 +676,9 @@ mod tests { ))); // Create account service with local wallet - let account_config = toml::from_str( - r#"private_key = "0x1234567890123456789012345678901234567890123456789012345678901234""#, - ) - .expect("Failed to parse account config"); + let account_config = serde_json::json!({ + "private_key": "0x1234567890123456789012345678901234567890123456789012345678901234" + }); let account = Arc::new(AccountService::new( solver_account::implementations::local::create_account(&account_config) .await @@ -755,7 +699,7 @@ mod tests { let discovery = Arc::new(DiscoveryService::new(std::collections::HashMap::new())); // Create order service - needs implementations and strategy - let strategy_config = toml::Value::Table(toml::value::Table::new()); + let strategy_config = serde_json::Value::Object(serde_json::Map::new()); let strategy = solver_order::implementations::strategies::simple::create_strategy(&strategy_config) .expect("Failed to create strategy"); @@ -768,7 +712,7 @@ mod tests { let settlement = Arc::new(SettlementService::new(std::collections::HashMap::new(), 20)); // Create pricing service with mock implementation - let pricing_config = toml::Value::Table(toml::value::Table::new()); + let pricing_config = serde_json::Value::Object(serde_json::Map::new()); let pricing_impl = solver_pricing::implementations::mock::create_mock_pricing(&pricing_config) .expect("Failed to create mock pricing"); diff --git a/crates/solver-delivery/Cargo.toml b/crates/solver-delivery/Cargo.toml index 8e594206..f6438c81 100644 --- a/crates/solver-delivery/Cargo.toml +++ b/crates/solver-delivery/Cargo.toml @@ -26,7 +26,6 @@ solver-account = { path = "../solver-account" } solver-types = { path = "../solver-types" } thiserror = "2.0.17" tokio = { version = "1.0", features = ["rt-multi-thread"] } -toml = { workspace = true } tracing = "0.1" [features] diff --git a/crates/solver-delivery/src/implementations/evm/alloy.rs b/crates/solver-delivery/src/implementations/evm/alloy.rs index 2fca16cd..fa888ccb 100644 --- a/crates/solver-delivery/src/implementations/evm/alloy.rs +++ b/crates/solver-delivery/src/implementations/evm/alloy.rs @@ -142,14 +142,16 @@ pub struct AlloyDeliverySchema; impl AlloyDeliverySchema { /// Static validation method for use before instance creation - pub fn validate_config(config: &toml::Value) -> Result<(), solver_types::ValidationError> { + pub fn validate_config( + config: &serde_json::Value, + ) -> Result<(), solver_types::ValidationError> { let instance = Self; instance.validate(config) } } impl ConfigSchema for AlloyDeliverySchema { - fn validate(&self, config: &toml::Value) -> Result<(), solver_types::ValidationError> { + fn validate(&self, config: &serde_json::Value) -> Result<(), solver_types::ValidationError> { let schema = Schema::new( // Required fields vec![Field::new( @@ -178,7 +180,7 @@ impl ConfigSchema for AlloyDeliverySchema { )), ) .with_validator(|value| { - if let Some(table) = value.as_table() { + if let Some(table) = value.as_object() { // Validate that keys are valid integers (network IDs) // and values are strings (account names) for (key, val) in table { @@ -187,7 +189,7 @@ impl ConfigSchema for AlloyDeliverySchema { return Err(format!("Invalid network ID in accounts: {key}")); } // Check value is a string - if !val.is_str() { + if val.as_str().is_none() { return Err(format!("Account name for network {key} must be a string")); } } @@ -574,7 +576,7 @@ async fn monitor_transaction( /// # Returns /// A boxed implementation of DeliveryInterface configured for the specified networks pub fn create_http_delivery( - config: &toml::Value, + config: &serde_json::Value, networks: &NetworksConfig, default_signer: &AccountSigner, network_signers: &HashMap, @@ -589,7 +591,7 @@ pub fn create_http_delivery( .and_then(|v| v.as_array()) .map(|arr| { arr.iter() - .filter_map(|v| v.as_integer().map(|i| i as u64)) + .filter_map(|v| v.as_i64().map(|i| i as u64)) .collect::>() }) .ok_or_else(|| DeliveryError::Network("network_ids is required".to_string()))?; @@ -684,11 +686,11 @@ mod tests { #[test] fn test_config_schema_validation_valid() { let schema = AlloyDeliverySchema; - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "network_ids".to_string(), - toml::Value::Array(vec![toml::Value::Integer(1)]), + serde_json::Value::Array(vec![serde_json::Value::from(1)]), ); table }); @@ -700,9 +702,9 @@ mod tests { #[test] fn test_config_schema_validation_empty_network_ids() { let schema = AlloyDeliverySchema; - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); - table.insert("network_ids".to_string(), toml::Value::Array(vec![])); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); + table.insert("network_ids".to_string(), serde_json::Value::Array(vec![])); table }); @@ -716,11 +718,11 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn test_create_http_delivery_success() { - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "network_ids".to_string(), - toml::Value::Array(vec![toml::Value::Integer(1)]), + serde_json::Value::Array(vec![serde_json::Value::from(1)]), ); table }); @@ -1012,7 +1014,7 @@ mod tests { #[test] fn test_config_missing_network_ids() { let schema = AlloyDeliverySchema; - let config = toml::Value::Table(toml::map::Map::new()); + let config = serde_json::Value::Object(serde_json::Map::new()); let result = schema.validate(&config); assert!(result.is_err()); @@ -1021,11 +1023,11 @@ mod tests { #[test] fn test_config_network_ids_wrong_type() { let schema = AlloyDeliverySchema; - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "network_ids".to_string(), - toml::Value::String("not an array".to_string()), + serde_json::Value::String("not an array".to_string()), ); table }); @@ -1037,14 +1039,14 @@ mod tests { #[test] fn test_config_multiple_network_ids() { let schema = AlloyDeliverySchema; - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "network_ids".to_string(), - toml::Value::Array(vec![ - toml::Value::Integer(1), - toml::Value::Integer(137), - toml::Value::Integer(42161), + serde_json::Value::Array(vec![ + serde_json::Value::from(1), + serde_json::Value::from(137), + serde_json::Value::from(42161), ]), ); table @@ -1059,20 +1061,20 @@ mod tests { let schema = AlloyDeliverySchema; // Valid config should pass - let valid_config = toml::Value::Table({ - let mut table = toml::map::Map::new(); + let valid_config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "network_ids".to_string(), - toml::Value::Array(vec![toml::Value::Integer(1)]), + serde_json::Value::Array(vec![serde_json::Value::from(1)]), ); table }); assert!(schema.validate(&valid_config).is_ok()); // Invalid config (empty array) should fail - let invalid_config = toml::Value::Table({ - let mut table = toml::map::Map::new(); - table.insert("network_ids".to_string(), toml::Value::Array(vec![])); + let invalid_config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); + table.insert("network_ids".to_string(), serde_json::Value::Array(vec![])); table }); assert!(schema.validate(&invalid_config).is_err()); diff --git a/crates/solver-delivery/src/lib.rs b/crates/solver-delivery/src/lib.rs index 3327f86e..75dd8234 100644 --- a/crates/solver-delivery/src/lib.rs +++ b/crates/solver-delivery/src/lib.rs @@ -181,7 +181,7 @@ pub trait DeliveryInterface: Send + Sync { /// This is the function signature that all delivery implementations must provide /// to create instances of their delivery interface. pub type DeliveryFactory = fn( - &toml::Value, + &serde_json::Value, &NetworksConfig, &solver_account::AccountSigner, // Default/primary signer &HashMap, // Per-network signers diff --git a/crates/solver-demo/Cargo.toml b/crates/solver-demo/Cargo.toml index 116854d9..0a39f60e 100644 --- a/crates/solver-demo/Cargo.toml +++ b/crates/solver-demo/Cargo.toml @@ -39,11 +39,12 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" solver-account = { path = "../solver-account" } solver-config = { path = "../solver-config" } +solver-service = { path = "../solver-service" } +solver-storage = { path = "../solver-storage" } solver-types = { path = "../solver-types" } sysinfo = "0.32" thiserror = "2" tokio = { version = "1.41", features = ["full"] } -toml = "0.9" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } url = "2" diff --git a/crates/solver-demo/README.md b/crates/solver-demo/README.md index 3bfec4a1..08b67b6a 100644 --- a/crates/solver-demo/README.md +++ b/crates/solver-demo/README.md @@ -91,8 +91,8 @@ alias oif-demo='cargo run --bin solver-demo --' #### 1. Initialize Environment ```bash # Create and load configuration -oif-demo init new config/demo.toml --force -oif-demo init load config/demo.toml --local +oif-demo init new config/demo.json --force +oif-demo init load config/demo.json --local # Start local chains oif-demo env start @@ -169,8 +169,13 @@ oif-demo intent build \ #### 1. Setup Environment ```bash -# Load testnet configuration -oif-demo init load config/testnet.toml +# Load runtime configuration from the same storage backend used by solver-service +export STORAGE_BACKEND=redis +export REDIS_URL=redis://127.0.0.1:6379 +export SOLVER_ID=your-solver-id +oif-demo init load-storage +# or: +oif-demo init load-storage --solver-id your-solver-id # List tokens and accounts oif-demo token list @@ -265,6 +270,26 @@ oif-demo intent test ./.oif-demo/requests/post_orders.req.json ## Configuration +### Load From Running Solver Storage + +`oif-demo init load-storage` reads the persisted `OperatorConfig` from the active +storage backend (Redis/file/memory), converts it to runtime config, and initializes +the demo session. + +```bash +# Same backend variables used by solver-service +export STORAGE_BACKEND=redis +export REDIS_URL=redis://127.0.0.1:6379 +export SOLVER_ID=your-solver-id + +oif-demo init load-storage +``` + +Notes: +- If `--solver-id` is omitted, `SOLVER_ID` is used. +- The command looks up key `-operator`. +- For cross-process usage, use `redis` or `file`; `memory` is process-local. + ### Environment Variables (`.env`) ```bash # User Account (creates intents) @@ -291,6 +316,9 @@ oif-demo init new [--force] [--chains ] # Load configuration oif-demo init load [--local] + +# Load configuration from storage backend +oif-demo init load-storage [--solver-id ] [--local] ``` ### Environment Commands @@ -489,17 +517,16 @@ Using simulated fill gas: 79850 units (config default was: 77298 units) Solvers can configure callback safety checks in their config file: -```toml -[order] -# Whitelisted callback contract addresses in EIP-7930 InteropAddress format -# Format: "0x" + Version(2 bytes) + ChainType(2 bytes) + ChainRefLen(1 byte) + ChainRef + AddrLen(1 byte) + Address -callback_whitelist = [ - "0x0001000002210514154c8bb598df835e9617c2cdcb8c84838bd329c6", # Base (8453) - "0x0001000003014a3414154c8bb598df835e9617c2cdcb8c84838bd329c6", # Base Sepolia (84532) -] - -# Enable gas simulation for callbacks before filling (default: true) -simulate_callbacks = true +```json +{ + "order": { + "callback_whitelist": [ + "0x0001000002210514154c8bb598df835e9617c2cdcb8c84838bd329c6", + "0x0001000003014a3414154c8bb598df835e9617c2cdcb8c84838bd329c6" + ], + "simulate_callbacks": true + } +} ``` **EIP-7930 InteropAddress Format:** @@ -551,4 +578,4 @@ oif-demo env deploy --contract MyToken --chain 31337 ```bash # Submit directly to blockchain (bypasses API) oif-demo intent submit order.json --onchain -``` \ No newline at end of file +``` diff --git a/crates/solver-demo/src/bin/solver-demo.rs b/crates/solver-demo/src/bin/solver-demo.rs index 78f6539d..aa04ee2d 100644 --- a/crates/solver-demo/src/bin/solver-demo.rs +++ b/crates/solver-demo/src/bin/solver-demo.rs @@ -131,6 +131,29 @@ async fn handle_init(cmd: solver_demo::cli::commands::InitCommand) -> Result<()> logging::success(&format!("Configuration loaded ({env_type})")); logging::success("Session initialized"); + if local { + logging::next_step("Start environment with 'oif-demo env start'"); + } + }, + InitSubcommand::LoadStorage { solver_id, local } => { + let solver_id_hint = solver_id.clone().unwrap_or_else(|| "SOLVER_ID".to_string()); + logging::operation_start(&format!( + "Loading configuration from storage for solver {solver_id_hint}..." + )); + logging::verbose_operation("Local mode", &local.to_string()); + + let init_ops = InitOps::without_context(); + let generated_path = init_ops.load_from_storage(solver_id, local).await?; + + let env_type = if local { + "local mode" + } else { + "production mode" + }; + logging::success(&format!("Configuration loaded from storage ({env_type})")); + logging::info_kv("Generated config", &generated_path.display().to_string()); + logging::success("Session initialized"); + if local { logging::next_step("Start environment with 'oif-demo env start'"); } @@ -160,7 +183,7 @@ async fn handle_config() -> Result<()> { Err(_) => { logging::warning("No configuration loaded"); logging::next_step( - "Create with 'oif-demo init new ' or load with 'oif-demo init load '", + "Create with 'oif-demo init new ', load file with 'oif-demo init load ', or load storage with 'oif-demo init load-storage --solver-id '", ); }, } diff --git a/crates/solver-demo/src/cli/commands/init.rs b/crates/solver-demo/src/cli/commands/init.rs index abd10f8a..2afc6d9f 100644 --- a/crates/solver-demo/src/cli/commands/init.rs +++ b/crates/solver-demo/src/cli/commands/init.rs @@ -45,4 +45,15 @@ pub enum InitSubcommand { #[arg(long)] local: bool, }, + + /// Load configuration from storage backend (Redis/file/memory) + LoadStorage { + /// Solver ID to load (defaults to SOLVER_ID env var) + #[arg(long)] + solver_id: Option, + + /// Initialize for local environment + #[arg(long)] + local: bool, + }, } diff --git a/crates/solver-demo/src/core/config.rs b/crates/solver-demo/src/core/config.rs index a341ac12..51d7bbf7 100644 --- a/crates/solver-demo/src/core/config.rs +++ b/crates/solver-demo/src/core/config.rs @@ -53,8 +53,28 @@ impl Config { return Err(Error::ConfigNotFound(path.to_path_buf())); } - // Use SolverFullConfig::from_file to properly handle includes - let full_config = SolverFullConfig::from_file(path.to_str().unwrap()).await?; + let is_json = path + .extension() + .and_then(|ext| ext.to_str()) + .is_some_and(|ext| ext.eq_ignore_ascii_case("json")); + + let full_config = if is_json { + let content = std::fs::read_to_string(path).map_err(|e| { + Error::InvalidConfig(format!( + "Failed to read JSON config {}: {e}", + path.display() + )) + })?; + serde_json::from_str::(&content).map_err(|e| { + Error::InvalidConfig(format!( + "Invalid JSON config format in {}: {e}", + path.display() + )) + })? + } else { + // Use SolverFullConfig::from_file to properly handle includes + SolverFullConfig::from_file(path.to_str().unwrap()).await? + }; // Extract networks from the full config let mut networks = HashMap::new(); diff --git a/crates/solver-demo/src/core/session.rs b/crates/solver-demo/src/core/session.rs index 48021a8b..2437d8d2 100644 --- a/crates/solver-demo/src/core/session.rs +++ b/crates/solver-demo/src/core/session.rs @@ -48,41 +48,12 @@ fn build_config_sections_mapping(config_path: &Path) -> HashMap // Always map "main" to the main config file sections.insert("main".to_string(), config_path.to_path_buf()); - // Try to read and parse the main config file + // Try to read and parse the main JSON config file. if let Ok(content) = std::fs::read_to_string(config_path) { - if let Ok(main_config) = toml::from_str::(&content) { - // Check for includes array - if let Some(includes) = main_config.get("include").and_then(|v| v.as_array()) { - let config_dir = config_path.parent().unwrap_or(Path::new(".")); - - for include_value in includes { - if let Some(include_path_str) = include_value.as_str() { - let include_path = config_dir.join(include_path_str); - - // Read the included file and determine what sections it contains - if let Ok(include_content) = std::fs::read_to_string(&include_path) { - if let Ok(include_config) = - toml::from_str::(&include_content) - { - // Map each top-level section in the included file - if let Some(table) = include_config.as_table() { - for section_name in table.keys() { - sections.insert(section_name.clone(), include_path.clone()); - } - } - } - } - } - } - } - - // Also map sections that are directly in the main config file - if let Some(table) = main_config.as_table() { + if let Ok(main_config) = serde_json::from_str::(&content) { + if let Some(table) = main_config.as_object() { for section_name in table.keys() { - // Don't override if already mapped to an include file - if !sections.contains_key(section_name) && section_name != "include" { - sections.insert(section_name.clone(), config_path.to_path_buf()); - } + sections.insert(section_name.clone(), config_path.to_path_buf()); } } } diff --git a/crates/solver-demo/src/lib.rs b/crates/solver-demo/src/lib.rs index decadeca..c86dd66b 100644 --- a/crates/solver-demo/src/lib.rs +++ b/crates/solver-demo/src/lib.rs @@ -242,16 +242,14 @@ impl Context { let data_dir = std::path::Path::new(".oif-demo"); if !data_dir.exists() { return Err(Error::InvalidConfig( - "No configuration loaded. Run 'cargo run -p solver-demo -- init load ' first" - .to_string(), + "No configuration loaded. Run 'cargo run -p solver-demo -- init load ' or 'cargo run -p solver-demo -- init load-storage --solver-id ' first".to_string(), )); } let storage = Storage::new(data_dir)?; if !storage.exists("session") { return Err(Error::InvalidConfig( - "No session found. Run 'cargo run -p solver-demo init load ' first" - .to_string(), + "No session found. Run 'cargo run -p solver-demo init load ' or 'cargo run -p solver-demo init load-storage --solver-id ' first".to_string(), )); } @@ -259,21 +257,19 @@ impl Context { let session_store = SessionStore::load(storage.clone())?; let config_path = session_store.config_path().ok_or_else(|| { Error::InvalidConfig( - "No config path stored. Run 'cargo run -p solver-demo init load ' first" - .to_string(), + "No config path stored. Run 'cargo run -p solver-demo init load ' or 'cargo run -p solver-demo init load-storage --solver-id ' first".to_string(), ) })?; if config_path.as_os_str().is_empty() { return Err(Error::InvalidConfig( - "No config path stored. Run 'cargo run -p solver-demo init load ' first" - .to_string(), + "No config path stored. Run 'cargo run -p solver-demo init load ' or 'cargo run -p solver-demo init load-storage --solver-id ' first".to_string(), )); } if !config_path.exists() { return Err(Error::InvalidConfig(format!( - "Config file not found: {}. Run 'cargo run -p solver-demo init load ' again", + "Config file not found: {}. Run 'cargo run -p solver-demo init load ' or 'cargo run -p solver-demo init load-storage --solver-id ' again", config_path.display() ))); } diff --git a/crates/solver-demo/src/operations/env/mod.rs b/crates/solver-demo/src/operations/env/mod.rs index 99247744..eb0784fd 100644 --- a/crates/solver-demo/src/operations/env/mod.rs +++ b/crates/solver-demo/src/operations/env/mod.rs @@ -229,9 +229,9 @@ impl EnvOps { .session .set_contract_addresses(chain, addresses.clone())?; - // Update TOML files with deployed addresses if in local mode + // Update JSON files with deployed addresses if in local mode if self.ctx.is_local() { - // For each deployed contract, inject its address into the appropriate TOML file + // For each deployed contract, inject its address into the appropriate JSON file if let Some(address) = addresses.input_settler { self.replace_config_placeholders(chain, "InputSettler", address) .await?; @@ -352,16 +352,16 @@ impl EnvOps { &format!("{contract_name} on chain {chain} at {address}"), ); - // Handle TOML updates based on environment + // Handle JSON updates based on environment if self.ctx.is_local() { - // Local: automatically inject into TOML + // Local: automatically inject into JSON self.replace_config_placeholders(chain, contract_name, address) .await?; } else { // Production: log that manual update is needed logging::verbose_operation( "Production deployment", - &format!("manual TOML update required for {contract_name} on chain {chain}"), + &format!("manual JSON update required for {contract_name} on chain {chain}"), ); } } @@ -647,9 +647,9 @@ impl EnvOps { Ok(()) } - /// Inject a deployed contract address into the appropriate TOML config file + /// Inject a deployed contract address into the appropriate JSON config file /// This function looks up the placeholder address and replaces it with the actual address - pub async fn inject_address_to_toml( + pub async fn inject_address_to_json( &self, placeholder_key: &str, actual_address: &str, @@ -671,7 +671,7 @@ impl EnvOps { let placeholder_address = placeholder_address.unwrap(); - // Determine which TOML file to update based on the placeholder prefix + // Determine which JSON file to update based on the placeholder prefix let target_file = if placeholder_key.contains("SETTLEMENT") { // Settlement domain goes in the main config file (settlement section) if let Some(settlement_file) = config_sections.get("settlement") { @@ -777,8 +777,8 @@ impl EnvOps { _ => return Ok(()), // Unknown contract, skip }; - // Inject the address into the appropriate TOML file - self.inject_address_to_toml(&placeholder_key, &address.to_string()) + // Inject the address into the appropriate JSON file + self.inject_address_to_json(&placeholder_key, &address.to_string()) .await } } diff --git a/crates/solver-demo/src/operations/init/mod.rs b/crates/solver-demo/src/operations/init/mod.rs index bbd91ec1..765e1e7f 100644 --- a/crates/solver-demo/src/operations/init/mod.rs +++ b/crates/solver-demo/src/operations/init/mod.rs @@ -17,6 +17,12 @@ use crate::{ }; use alloy_primitives::Address; use solver_config::SettlementConfig; +use solver_service::config_merge::build_runtime_config; +use solver_storage::{ + config_store::{create_config_store, ConfigStoreError}, + StoreConfig, +}; +use solver_types::OperatorConfig; use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -88,6 +94,27 @@ impl InitOps { // Delegate to the existing load_config function load_config(&path, is_local).await } + + /// Load configuration from runtime storage (Redis/file/memory) and initialize session + /// + /// # Arguments + /// * `solver_id` - Solver ID to load (falls back to `SOLVER_ID` env when None) + /// * `is_local` - Whether to initialize for local development environment + /// + /// # Returns + /// Path to the generated runtime configuration file + /// + /// # Errors + /// Returns Error if solver ID cannot be resolved, storage cannot be read, + /// config conversion fails, or session initialization fails. + #[instrument(skip(self))] + pub async fn load_from_storage( + &self, + solver_id: Option, + is_local: bool, + ) -> Result { + load_config_from_storage(solver_id, is_local).await + } } /// Generate placeholder contract addresses for configuration templates @@ -189,30 +216,9 @@ pub async fn generate_new_config(path: &Path, chains: Vec, force: bool) -> // Generate configuration content using same format as original let config_content = generate_demo_config(&chains, config_name, &placeholders)?; - // Write main config file + // Write single JSON config file std::fs::write(path, config_content)?; - // Create includes directory - if let Some(parent) = path.parent() { - let include_dir = parent.join(config_name); - std::fs::create_dir_all(&include_dir)?; - - // Write networks.toml - let networks_path = include_dir.join("networks.toml"); - std::fs::write( - networks_path, - generate_networks_config(&chains, &placeholders)?, - )?; - - // Write gas.toml - let gas_path = include_dir.join("gas.toml"); - std::fs::write(gas_path, generate_gas_config()?)?; - - // Write api.toml - let api_path = include_dir.join("api.toml"); - std::fs::write(api_path, generate_api_config()?)?; - } - Ok(()) } @@ -330,6 +336,124 @@ pub async fn load_config(path: &Path, is_local: bool) -> Result<()> { Ok(()) } +/// Load configuration from runtime storage and initialize session. +/// +/// Reads `OperatorConfig` from the configured storage backend using the same key +/// format as the solver service, converts it into runtime `Config`, materializes +/// that config to `.oif-demo/config`, and then initializes session state. +pub async fn load_config_from_storage( + solver_id: Option, + is_local: bool, +) -> Result { + let solver_id = resolve_solver_id(solver_id)?; + let store_config = StoreConfig::from_env() + .map_err(|e| Error::InvalidConfig(format!("Failed to read storage configuration: {e}")))?; + let store_key = operator_config_store_key(&solver_id); + + let config_store = create_config_store::(store_config, store_key.clone()) + .map_err(|e| { + Error::InvalidConfig(format!( + "Failed to create config store for solver '{solver_id}': {e}" + )) + })?; + + let versioned = config_store.get().await.map_err(|e| match e { + ConfigStoreError::NotFound(_) => Error::InvalidConfig(format!( + "Operator config not found in storage for solver '{solver_id}' (store key: {store_key})" + )), + other => Error::InvalidConfig(format!( + "Failed to load operator config from storage for solver '{solver_id}': {other}" + )), + })?; + + let runtime_config = build_runtime_config(&versioned.data).map_err(|e| { + Error::InvalidConfig(format!( + "Failed to convert operator config to runtime config for solver '{solver_id}' (version {}): {e}", + versioned.version + )) + })?; + + let generated_config_path = generated_runtime_config_path(&solver_id); + materialize_runtime_config(&runtime_config, &generated_config_path)?; + + load_config(&generated_config_path, is_local).await?; + Ok(generated_config_path) +} + +fn resolve_solver_id(solver_id: Option) -> Result { + if let Some(id) = solver_id { + let trimmed = id.trim(); + if trimmed.is_empty() { + return Err(Error::InvalidConfig( + "Solver ID cannot be empty".to_string(), + )); + } + return Ok(trimmed.to_string()); + } + + let env_id = std::env::var("SOLVER_ID").map_err(|_| { + Error::InvalidConfig( + "Solver ID not provided. Use --solver-id or set SOLVER_ID".to_string(), + ) + })?; + let trimmed = env_id.trim(); + if trimmed.is_empty() { + return Err(Error::InvalidConfig( + "SOLVER_ID is set but empty".to_string(), + )); + } + Ok(trimmed.to_string()) +} + +fn operator_config_store_key(solver_id: &str) -> String { + format!("{solver_id}-operator") +} + +fn generated_runtime_config_path(solver_id: &str) -> PathBuf { + let safe_solver_id = sanitize_solver_id_for_filename(solver_id); + PathBuf::from(".oif-demo") + .join("config") + .join(format!("{safe_solver_id}.json")) +} + +fn sanitize_solver_id_for_filename(solver_id: &str) -> String { + let sanitized: String = solver_id + .chars() + .map(|c| { + if c.is_ascii_alphanumeric() || c == '-' || c == '_' { + c + } else { + '_' + } + }) + .collect(); + + if sanitized.is_empty() { + "solver".to_string() + } else { + sanitized + } +} + +fn materialize_runtime_config( + runtime_config: &solver_config::Config, + output_path: &Path, +) -> Result<()> { + if let Some(parent) = output_path.parent() { + std::fs::create_dir_all(parent)?; + } + + let json_content = serde_json::to_string_pretty(runtime_config).map_err(|e| { + Error::InvalidConfig(format!( + "Failed to serialize runtime config to JSON for {}: {e}", + output_path.display() + )) + })?; + + std::fs::write(output_path, json_content)?; + Ok(()) +} + /// Gets input oracle address for the specified chain fn get_input_oracle_for_chain( settlement_config: &SettlementConfig, @@ -340,13 +464,13 @@ fn get_input_oracle_for_chain( if let Some(network_ids) = network_ids_value.as_array() { let has_chain = network_ids .iter() - .any(|id| id.as_integer().is_some_and(|i| i as u64 == chain_id)); + .any(|id| id.as_i64().is_some_and(|i| i as u64 == chain_id)); if has_chain { if let Some(oracles_value) = impl_config.get("oracles") { - if let Some(oracles_table) = oracles_value.as_table() { + if let Some(oracles_table) = oracles_value.as_object() { if let Some(input_value) = oracles_table.get("input") { - if let Some(input_table) = input_value.as_table() { + if let Some(input_table) = input_value.as_object() { if let Some(oracles_array) = input_table.get(&chain_id.to_string()) { @@ -379,13 +503,13 @@ fn get_output_oracle_for_chain( if let Some(network_ids) = network_ids_value.as_array() { let has_chain = network_ids .iter() - .any(|id| id.as_integer().is_some_and(|i| i as u64 == chain_id)); + .any(|id| id.as_i64().is_some_and(|i| i as u64 == chain_id)); if has_chain { if let Some(oracles_value) = impl_config.get("oracles") { - if let Some(oracles_table) = oracles_value.as_table() { + if let Some(oracles_table) = oracles_value.as_object() { if let Some(output_value) = oracles_table.get("output") { - if let Some(output_table) = output_value.as_table() { + if let Some(output_table) = output_value.as_object() { if let Some(oracles_array) = output_table.get(&chain_id.to_string()) { @@ -411,279 +535,24 @@ fn get_output_oracle_for_chain( /// Generate the main configuration file fn generate_demo_config( chain_ids: &[u64], - config_name: &str, + _config_name: &str, placeholders: &HashMap, ) -> Result { - let mut config = String::new(); - - // Header - config.push_str("# OIF Solver Configuration - Generated File\n"); - config.push_str("# Generated with placeholder values for easy regex replacement\n\n"); - - // Include files - config.push_str(&format!( - "include = [\"{config_name}/networks.toml\", \"{config_name}/api.toml\", \"{config_name}/gas.toml\"]\n\n" - )); - - // Solver configuration - config.push_str("[solver]\n"); - config.push_str("id = \"oif-solver-demo\"\n"); - config.push_str("min_profitability_pct = 1.0\n"); - config.push_str("monitoring_timeout_seconds = 28800\n\n"); - - // Storage configuration - add_storage_config(&mut config); - - // Account configuration - add_account_config(&mut config); - - // Delivery configuration - add_delivery_config(&mut config, chain_ids); - - // Discovery configuration - add_discovery_config(&mut config, chain_ids); - - // Order configuration - add_order_config(&mut config); - - // Pricing configuration - add_pricing_config(&mut config); - - // Settlement configuration - add_settlement_config(&mut config, chain_ids, placeholders)?; - - Ok(config) -} - -fn add_storage_config(config: &mut String) { - config.push_str( - "# ============================================================================\n", - ); - config.push_str("# STORAGE\n"); - config.push_str( - "# ============================================================================\n", - ); - config.push_str("[storage]\n"); - config.push_str("primary = \"file\"\n"); - config.push_str("cleanup_interval_seconds = 60\n\n"); - - config.push_str("[storage.implementations.memory]\n"); - config.push_str("# Memory storage has no configuration\n\n"); - - config.push_str("[storage.implementations.file]\n"); - config.push_str("storage_path = \"./data/storage\"\n"); - config.push_str("ttl_orders = 300 # 5 minutes\n"); - config.push_str("ttl_intents = 120 # 2 minutes\n"); - config.push_str("ttl_order_by_tx_hash = 300 # 5 minutes\n\n"); -} - -fn add_account_config(config: &mut String) { - // Load environment variables - let _ = dotenvy::dotenv(); - - config.push_str( - "# ============================================================================\n", - ); - config.push_str("# ACCOUNT\n"); - config.push_str( - "# ============================================================================\n", - ); - config.push_str("[account]\n"); - config.push_str("primary = \"local\"\n\n"); - - config.push_str("[account.implementations.local]\n"); - - // Use SOLVER_PRIVATE_KEY from env or default Anvil key let solver_key = std::env::var(env_vars::SOLVER_PRIVATE_KEY) .unwrap_or_else(|_| anvil_accounts::SOLVER_PRIVATE_KEY.to_string()); - config.push_str(&format!("private_key = \"{solver_key}\"\n\n")); -} - -fn add_delivery_config(config: &mut String, chain_ids: &[u64]) { - config.push_str( - "# ============================================================================\n", - ); - config.push_str("# DELIVERY\n"); - config.push_str( - "# ============================================================================\n", - ); - config.push_str("[delivery]\n"); - config.push_str("min_confirmations = 1\n\n"); - - config.push_str("[delivery.implementations.evm_alloy]\n"); - config.push_str(&format!("network_ids = {chain_ids:?}\n\n")); -} - -fn add_discovery_config(config: &mut String, chain_ids: &[u64]) { - config.push_str( - "# ============================================================================\n", - ); - config.push_str("# DISCOVERY\n"); - config.push_str( - "# ============================================================================\n", - ); - config.push_str("[discovery]\n\n"); - - config.push_str("[discovery.implementations.onchain_eip7683]\n"); - config.push_str(&format!("network_ids = {chain_ids:?}\n")); - config.push_str( - "polling_interval_secs = 0 # Use WebSocket subscriptions instead of polling\n\n", - ); - - config.push_str("[discovery.implementations.offchain_eip7683]\n"); - config.push_str("api_host = \"127.0.0.1\"\n"); - config.push_str("api_port = 8081\n"); - config.push_str(&format!( - "network_ids = [{}]\n\n", - chain_ids.first().unwrap_or(&31337) - )); -} - -fn add_order_config(config: &mut String) { - config.push_str( - "# ============================================================================\n", - ); - config.push_str("# ORDER\n"); - config.push_str( - "# ============================================================================\n", - ); - config.push_str("[order]\n\n"); - - config.push_str("[order.implementations.eip7683]\n\n"); - - config.push_str("[order.strategy]\n"); - config.push_str("primary = \"simple\"\n\n"); - - config.push_str("[order.strategy.implementations.simple]\n"); - config.push_str("max_gas_price_gwei = 100\n\n"); -} - -fn add_pricing_config(config: &mut String) { - config.push_str( - "# ============================================================================\n", - ); - config.push_str("# PRICING\n"); - config.push_str( - "# ============================================================================\n", - ); - config.push_str("[pricing]\n"); - config.push_str("primary = \"mock\"\n\n"); - - config.push_str("[pricing.implementations.mock]\n"); - config.push_str("# Uses default ETH/USD price of 4615.16\n\n"); - - config.push_str("[pricing.implementations.coingecko]\n"); - config.push_str("# Free tier configuration (no API key required)\n"); - config.push_str("# api_key = \"CG-YOUR-API-KEY-HERE\"\n"); - config.push_str("cache_duration_seconds = 60\n"); - config.push_str("rate_limit_delay_ms = 1200\n\n"); - - config.push_str("# Custom prices for demo/test tokens (in USD)\n"); - config.push_str("[pricing.implementations.coingecko.custom_prices]\n"); - config.push_str("TOKA = \"200.00\"\n"); - config.push_str("TOKB = \"195.00\"\n\n"); -} - -fn add_settlement_config( - config: &mut String, - chain_ids: &[u64], - placeholders: &HashMap, -) -> Result<()> { - config.push_str( - "# ============================================================================\n", - ); - config.push_str("# SETTLEMENT\n"); - config.push_str( - "# ============================================================================\n", - ); - config.push_str("[settlement]\n"); - config.push_str("settlement_poll_interval_seconds = 3\n\n"); - - config.push_str("[settlement.implementations.direct]\n"); - config.push_str("order = \"eip7683\"\n"); - config.push_str(&format!("network_ids = {chain_ids:?}\n")); - config.push_str("dispute_period_seconds = 1\n"); - config.push_str("# Oracle selection strategy when multiple oracles are available (First, RoundRobin, Random)\n"); - config.push_str("oracle_selection_strategy = \"First\"\n\n"); - - // Oracle configuration - config.push_str("# Oracle configuration with multiple oracle support\n"); - config.push_str("[settlement.implementations.direct.oracles]\n"); - - // Input oracles - config.push_str("# Input oracles (on origin chains)\n"); - config.push_str("input = { "); - for (i, chain_id) in chain_ids.iter().enumerate() { - if i > 0 { - config.push_str(", "); - } - let oracle_addr = placeholders - .get(&format!("ORACLE_PLACEHOLDER_INPUT_{chain_id}")) - .ok_or_else(|| { - Error::InvalidConfig(format!("Missing ORACLE_PLACEHOLDER_INPUT_{chain_id}")) - })?; - config.push_str(&format!("{chain_id} = [\n \"{oracle_addr}\",\n]")); - } - config.push_str(" }\n"); - - // Output oracles - config.push_str("# Output oracles (on destination chains)\n"); - config.push_str("output = { "); - for (i, chain_id) in chain_ids.iter().enumerate() { - if i > 0 { - config.push_str(", "); - } - let oracle_addr = placeholders - .get(&format!("ORACLE_PLACEHOLDER_OUTPUT_{chain_id}")) - .ok_or_else(|| { - Error::InvalidConfig(format!("Missing ORACLE_PLACEHOLDER_OUTPUT_{chain_id}")) - })?; - config.push_str(&format!("{chain_id} = [\n \"{oracle_addr}\",\n]")); - } - config.push_str(" }\n\n"); - - // Valid routes - config.push_str("# Valid routes: from origin chain -> to destination chains\n"); - config.push_str("[settlement.implementations.direct.routes]\n"); - - for from_chain in chain_ids { - let to_chains: Vec = chain_ids - .iter() - .filter(|&c| c != from_chain) - .cloned() - .collect(); - if !to_chains.is_empty() { - config.push_str(&format!("{from_chain} = {to_chains:?}\n")); - } - } - - Ok(()) -} -/// Generate networks.toml configuration -fn generate_networks_config( - chain_ids: &[u64], - placeholders: &HashMap, -) -> Result { - let mut config = String::new(); - - // Header - config.push_str("# Network Configuration - Generated with Placeholders\n"); - config.push_str("# Defines all supported blockchain networks and their tokens\n\n"); - - // Generate configuration for each chain + let mut networks = serde_json::Map::new(); for (idx, chain_id) in chain_ids.iter().enumerate() { - config.push_str(&format!("[networks.{chain_id}]\n")); - - // Contract addresses let input_settler = placeholders .get(&format!("PLACEHOLDER_INPUT_SETTLER_{chain_id}")) .ok_or_else(|| { Error::InvalidConfig(format!("Missing PLACEHOLDER_INPUT_SETTLER_{chain_id}")) })?; - config.push_str(&format!("input_settler_address = \"{input_settler}\"\n")); - - // InputSettlerCompact address + let output_settler = placeholders + .get(&format!("PLACEHOLDER_OUTPUT_SETTLER_{chain_id}")) + .ok_or_else(|| { + Error::InvalidConfig(format!("Missing PLACEHOLDER_OUTPUT_SETTLER_{chain_id}")) + })?; let input_settler_compact = placeholders .get(&format!("PLACEHOLDER_INPUT_SETTLER_COMPACT_{chain_id}")) .ok_or_else(|| { @@ -691,124 +560,689 @@ fn generate_networks_config( "Missing PLACEHOLDER_INPUT_SETTLER_COMPACT_{chain_id}" )) })?; - config.push_str(&format!( - "input_settler_compact_address = \"{input_settler_compact}\"\n" - )); - - // TheCompact contract address let compact = placeholders .get(&format!("PLACEHOLDER_COMPACT_{chain_id}")) .ok_or_else(|| { Error::InvalidConfig(format!("Missing PLACEHOLDER_COMPACT_{chain_id}")) })?; - config.push_str(&format!("the_compact_address = \"{compact}\"\n")); - let allocator = placeholders .get(&format!("PLACEHOLDER_ALLOCATOR_{chain_id}")) .ok_or_else(|| { Error::InvalidConfig(format!("Missing PLACEHOLDER_ALLOCATOR_{chain_id}")) })?; - config.push_str(&format!("allocator_address = \"{allocator}\"\n")); - - let output_settler = placeholders - .get(&format!("PLACEHOLDER_OUTPUT_SETTLER_{chain_id}")) - .ok_or_else(|| { - Error::InvalidConfig(format!("Missing PLACEHOLDER_OUTPUT_SETTLER_{chain_id}")) - })?; - config.push_str(&format!( - "output_settler_address = \"{output_settler}\"\n\n" - )); - - // RPC endpoints - config.push_str("# RPC endpoints with both HTTP and WebSocket URLs for each network\n"); - config.push_str(&format!("[[networks.{chain_id}.rpc_urls]]\n")); - - let port = 8545 + idx; - config.push_str(&format!("http = \"http://localhost:{port}\"\n")); - config.push_str(&format!("ws = \"ws://localhost:{port}\"\n\n")); - - // Token configurations - config.push_str(&format!("[[networks.{chain_id}.tokens]]\n")); let token_a = placeholders .get(&format!("PLACEHOLDER_TOKEN_A_{chain_id}")) .ok_or_else(|| { Error::InvalidConfig(format!("Missing PLACEHOLDER_TOKEN_A_{chain_id}")) })?; - config.push_str(&format!("address = \"{token_a}\"\n")); - config.push_str("symbol = \"TOKA\"\n"); - config.push_str(&format!("decimals = {DEFAULT_TOKEN_DECIMALS}\n\n")); - - config.push_str(&format!("[[networks.{chain_id}.tokens]]\n")); let token_b = placeholders .get(&format!("PLACEHOLDER_TOKEN_B_{chain_id}")) .ok_or_else(|| { Error::InvalidConfig(format!("Missing PLACEHOLDER_TOKEN_B_{chain_id}")) })?; - config.push_str(&format!("address = \"{token_b}\"\n")); - config.push_str("symbol = \"TOKB\"\n"); - config.push_str(&format!("decimals = {DEFAULT_TOKEN_DECIMALS}\n\n")); + + let port = 8545 + idx; + networks.insert( + chain_id.to_string(), + serde_json::json!({ + "input_settler_address": input_settler, + "output_settler_address": output_settler, + "input_settler_compact_address": input_settler_compact, + "the_compact_address": compact, + "allocator_address": allocator, + "rpc_urls": [{ + "http": format!("http://localhost:{port}"), + "ws": format!("ws://localhost:{port}") + }], + "tokens": [ + { + "address": token_a, + "symbol": "TOKA", + "decimals": DEFAULT_TOKEN_DECIMALS + }, + { + "address": token_b, + "symbol": "TOKB", + "decimals": DEFAULT_TOKEN_DECIMALS + } + ] + }), + ); } - Ok(config) + let routes = chain_ids + .iter() + .map(|from| { + let to: Vec = chain_ids.iter().copied().filter(|c| c != from).collect(); + (from.to_string(), serde_json::json!(to)) + }) + .collect::>(); + + let input_oracles = chain_ids + .iter() + .map(|chain_id| { + let addr = placeholders + .get(&format!("ORACLE_PLACEHOLDER_INPUT_{chain_id}")) + .cloned() + .unwrap_or_else(|| "0x0000000000000000000000000000000000000000".to_string()); + (chain_id.to_string(), serde_json::json!([addr])) + }) + .collect::>(); + + let output_oracles = chain_ids + .iter() + .map(|chain_id| { + let addr = placeholders + .get(&format!("ORACLE_PLACEHOLDER_OUTPUT_{chain_id}")) + .cloned() + .unwrap_or_else(|| "0x0000000000000000000000000000000000000000".to_string()); + (chain_id.to_string(), serde_json::json!([addr])) + }) + .collect::>(); + + let first_chain = *chain_ids.first().unwrap_or(&31337); + let config = serde_json::json!({ + "solver": { + "id": "oif-solver-demo", + "min_profitability_pct": "1.0", + "monitoring_timeout_seconds": 28800 + }, + "networks": serde_json::Value::Object(networks), + "storage": { + "primary": "file", + "cleanup_interval_seconds": 60, + "implementations": { + "memory": {}, + "file": { + "storage_path": "./data/storage", + "ttl_orders": 300, + "ttl_intents": 120, + "ttl_order_by_tx_hash": 300 + } + } + }, + "account": { + "primary": "local", + "implementations": { + "local": { "private_key": solver_key } + } + }, + "delivery": { + "min_confirmations": 1, + "implementations": { + "evm_alloy": { "network_ids": chain_ids } + } + }, + "discovery": { + "implementations": { + "onchain_eip7683": { + "network_ids": chain_ids, + "polling_interval_secs": 0 + }, + "offchain_eip7683": { + "api_host": "127.0.0.1", + "api_port": 8081, + "network_ids": [first_chain] + } + } + }, + "order": { + "implementations": { + "eip7683": {} + }, + "strategy": { + "primary": "simple", + "implementations": { + "simple": { + "max_gas_price_gwei": 100 + } + } + } + }, + "pricing": { + "primary": "mock", + "implementations": { + "mock": {}, + "coingecko": { + "cache_duration_seconds": 60, + "rate_limit_delay_ms": 1200, + "custom_prices": { + "TOKA": "200.00", + "TOKB": "195.00" + } + } + } + }, + "settlement": { + "settlement_poll_interval_seconds": 3, + "implementations": { + "direct": { + "order": "eip7683", + "network_ids": chain_ids, + "dispute_period_seconds": 1, + "oracle_selection_strategy": "First", + "oracles": { + "input": serde_json::Value::Object(input_oracles), + "output": serde_json::Value::Object(output_oracles) + }, + "routes": serde_json::Value::Object(routes) + } + } + }, + "api": { + "enabled": true, + "host": "127.0.0.1", + "port": 3000, + "timeout_seconds": 30, + "max_request_size": 1048576, + "implementations": { + "discovery": "offchain_eip7683" + }, + "auth": { + "enabled": true, + "jwt_secret": "${JWT_SECRET:-MySuperDuperSecureSecret123!}", + "access_token_expiry_hours": 1, + "refresh_token_expiry_hours": 720, + "issuer": "oif-solver-demo" + }, + "quote": { + "validity_seconds": 60 + } + }, + "gas": { + "flows": { + "resource_lock": { "open": 0, "fill": 77298, "claim": 122793 }, + "permit2_escrow": { "open": 146306, "fill": 77298, "claim": 60084 }, + "eip3009_escrow": { "open": 130254, "fill": 77298, "claim": 60084 } + } + } + }); + + serde_json::to_string_pretty(&config).map_err(Error::from) } -/// Generate gas.toml configuration -fn generate_gas_config() -> Result { - let mut config = String::new(); +#[cfg(test)] +mod tests { + use super::*; + use crate::core::{session::SessionStore, storage::Storage}; + use serde_json::json; + use tempfile::TempDir; + + use std::sync::{Mutex, OnceLock}; + + fn test_lock() -> &'static Mutex<()> { + static LOCK: OnceLock> = OnceLock::new(); + LOCK.get_or_init(|| Mutex::new(())) + } + + fn acquire_lock() -> std::sync::MutexGuard<'static, ()> { + match test_lock().lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + } + } - config.push_str("[gas]\n\n"); + struct EnvVarGuard { + key: &'static str, + original: Option, + } - config.push_str("[gas.flows.resource_lock]\n"); - config.push_str("# Gas units captured by scripts/e2e/estimate_gas.sh on local anvil\n"); - config.push_str("open = 0\n"); - config.push_str("fill = 77298\n"); - config.push_str("claim = 122793\n\n"); + impl EnvVarGuard { + fn set(key: &'static str, value: Option<&str>) -> Self { + let original = std::env::var(key).ok(); + match value { + Some(v) => std::env::set_var(key, v), + None => std::env::remove_var(key), + } + Self { key, original } + } + } - config.push_str("[gas.flows.permit2_escrow]\n"); - config.push_str("# Gas units captured by scripts/e2e/estimate_gas.sh on local anvil\n"); - config.push_str("open = 146306\n"); - config.push_str("fill = 77298\n"); - config.push_str("claim = 60084\n\n"); + impl Drop for EnvVarGuard { + fn drop(&mut self) { + match self.original.as_deref() { + Some(v) => std::env::set_var(self.key, v), + None => std::env::remove_var(self.key), + } + } + } - config.push_str("[gas.flows.eip3009_escrow]\n"); - config.push_str("# Gas units captured by scripts/e2e/estimate_gas.sh on local anvil\n"); - config.push_str("open = 130254\n"); - config.push_str("fill = 77298\n"); - config.push_str("claim = 60084\n"); + struct CwdGuard { + original: PathBuf, + } - Ok(config) -} + impl CwdGuard { + fn change_to(path: &Path) -> Self { + let original = std::env::current_dir().expect("read current dir"); + std::env::set_current_dir(path).expect("switch current dir"); + Self { original } + } + } + + impl Drop for CwdGuard { + fn drop(&mut self) { + std::env::set_current_dir(&self.original).expect("restore current dir"); + } + } + + fn test_settlement_config(value: serde_json::Value) -> SettlementConfig { + serde_json::from_value(value).expect("valid settlement config fixture") + } + + #[test] + fn generate_placeholder_map_has_expected_entries_and_sequence() { + let map = generate_placeholder_map(&[1, 10]); + + assert_eq!(map.len(), 18); + assert_eq!( + map.get("PLACEHOLDER_INPUT_SETTLER_1"), + Some(&format!("0x{PLACEHOLDER_START_COUNTER:040x}")) + ); + assert_eq!( + map.get("PLACEHOLDER_OUTPUT_SETTLER_1"), + Some(&format!("0x{:040x}", PLACEHOLDER_START_COUNTER + 1)) + ); + assert_eq!( + map.get("ORACLE_PLACEHOLDER_OUTPUT_1"), + Some(&format!("0x{:040x}", PLACEHOLDER_START_COUNTER + 8)) + ); + assert_eq!( + map.get("PLACEHOLDER_INPUT_SETTLER_10"), + Some(&format!("0x{:040x}", PLACEHOLDER_START_COUNTER + 9)) + ); + } + + #[test] + fn get_input_and_output_oracle_for_chain_returns_expected_oracles() { + let settlement = test_settlement_config(json!({ + "settlement_poll_interval_seconds": 3, + "implementations": { + "direct": { + "network_ids": [1, 2], + "oracles": { + "input": { + "1": ["0x1111111111111111111111111111111111111111"] + }, + "output": { + "1": ["0x2222222222222222222222222222222222222222"] + } + } + } + } + })); + + assert_eq!( + get_input_oracle_for_chain(&settlement, 1), + Some("0x1111111111111111111111111111111111111111".to_string()) + ); + assert_eq!( + get_output_oracle_for_chain(&settlement, 1), + Some("0x2222222222222222222222222222222222222222".to_string()) + ); + assert_eq!(get_input_oracle_for_chain(&settlement, 3), None); + assert_eq!(get_output_oracle_for_chain(&settlement, 3), None); + } + + #[test] + fn get_oracle_for_chain_returns_none_for_malformed_shapes() { + let settlement = test_settlement_config(json!({ + "settlement_poll_interval_seconds": 3, + "implementations": { + "direct": { + "network_ids": [1], + "oracles": { + "input": { "1": [123] }, + "output": { "1": "not-an-array" } + } + } + } + })); + + assert_eq!(get_input_oracle_for_chain(&settlement, 1), None); + assert_eq!(get_output_oracle_for_chain(&settlement, 1), None); + } + + #[test] + fn generate_demo_config_builds_expected_networks_and_routes() { + let _lock = acquire_lock(); + let placeholders = generate_placeholder_map(&[1, 10]); + let content = generate_demo_config(&[1, 10], "demo", &placeholders).expect("config json"); + let parsed: serde_json::Value = + serde_json::from_str(&content).expect("valid generated json"); + + let networks = parsed["networks"] + .as_object() + .expect("networks object is present"); + assert_eq!(networks.len(), 2); + assert_eq!( + parsed["networks"]["1"]["rpc_urls"][0]["http"], + "http://localhost:8545" + ); + assert_eq!( + parsed["networks"]["10"]["rpc_urls"][0]["http"], + "http://localhost:8546" + ); + assert_eq!(parsed["networks"]["1"]["tokens"][0]["symbol"], "TOKA"); + assert_eq!( + parsed["settlement"]["implementations"]["direct"]["routes"]["1"], + json!([10]) + ); + assert_eq!( + parsed["settlement"]["implementations"]["direct"]["routes"]["10"], + json!([1]) + ); + + let expected_solver_key = std::env::var(env_vars::SOLVER_PRIVATE_KEY) + .unwrap_or_else(|_| anvil_accounts::SOLVER_PRIVATE_KEY.to_string()); + assert_eq!( + parsed["account"]["implementations"]["local"]["private_key"], + expected_solver_key + ); + } + + #[test] + fn generate_demo_config_uses_env_solver_private_key_when_present() { + let _lock = acquire_lock(); + let _env_guard = EnvVarGuard::set( + env_vars::SOLVER_PRIVATE_KEY, + Some("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + ); + let placeholders = generate_placeholder_map(&[1]); + let content = generate_demo_config(&[1], "demo", &placeholders).expect("config json"); + let parsed: serde_json::Value = + serde_json::from_str(&content).expect("valid generated json"); + + assert_eq!( + parsed["account"]["implementations"]["local"]["private_key"], + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ); + } + + #[test] + fn generate_demo_config_returns_error_when_required_placeholder_missing() { + let mut placeholders = generate_placeholder_map(&[1]); + placeholders.remove("PLACEHOLDER_TOKEN_B_1"); + + let err = generate_demo_config(&[1], "demo", &placeholders).unwrap_err(); + match err { + Error::InvalidConfig(message) => { + assert!(message.contains("Missing PLACEHOLDER_TOKEN_B_1")); + }, + other => panic!("expected InvalidConfig, got {other:?}"), + } + } + + #[test] + fn generate_demo_config_uses_zero_oracle_when_oracle_placeholder_missing() { + let mut placeholders = generate_placeholder_map(&[1]); + placeholders.remove("ORACLE_PLACEHOLDER_INPUT_1"); + placeholders.remove("ORACLE_PLACEHOLDER_OUTPUT_1"); + + let content = generate_demo_config(&[1], "demo", &placeholders).expect("config json"); + let parsed: serde_json::Value = + serde_json::from_str(&content).expect("valid generated json"); -/// Generate api.toml configuration -fn generate_api_config() -> Result { - let mut config = String::new(); - - config.push_str("# API Server Configuration\n"); - config.push_str("# Configures the HTTP API for receiving off-chain intents\n\n"); - - config.push_str("[api]\n"); - config.push_str("enabled = true\n"); - config.push_str("host = \"127.0.0.1\"\n"); - config.push_str("port = 3000\n"); - config.push_str("timeout_seconds = 30\n"); - config.push_str("max_request_size = 1048576 # 1MB\n\n"); - - config.push_str("[api.implementations]\n"); - config.push_str("discovery = \"offchain_eip7683\"\n\n"); - - config.push_str("# JWT Authentication Configuration\n"); - config.push_str("[api.auth]\n"); - config.push_str("enabled = true\n"); - config.push_str("jwt_secret = \"${JWT_SECRET:-MySuperDuperSecureSecret123!}\"\n"); - config.push_str("access_token_expiry_hours = 1\n"); - config.push_str("refresh_token_expiry_hours = 720 # 30 days\n"); - config.push_str("issuer = \"oif-solver-demo\"\n\n"); - - config.push_str("# Quote Configuration\n"); - config.push_str("[api.quote]\n"); - config.push_str("# Quote validity duration in seconds\n"); - config.push_str("# Default is 20 seconds. Customize as needed:\n"); - config.push_str("validity_seconds = 60 # 1 minute validity\n"); - - Ok(config) + assert_eq!( + parsed["settlement"]["implementations"]["direct"]["oracles"]["input"]["1"], + json!(["0x0000000000000000000000000000000000000000"]) + ); + assert_eq!( + parsed["settlement"]["implementations"]["direct"]["oracles"]["output"]["1"], + json!(["0x0000000000000000000000000000000000000000"]) + ); + } + + #[test] + fn generate_demo_config_empty_chains_uses_default_offchain_chain_id() { + let _lock = acquire_lock(); + let content = generate_demo_config(&[], "demo", &HashMap::new()).expect("config json"); + let parsed: serde_json::Value = + serde_json::from_str(&content).expect("valid generated json"); + + assert_eq!( + parsed["discovery"]["implementations"]["offchain_eip7683"]["network_ids"], + json!([31337]) + ); + assert_eq!( + parsed["delivery"]["implementations"]["evm_alloy"]["network_ids"], + json!([]) + ); + } + + #[tokio::test] + #[allow(clippy::await_holding_lock)] + async fn generate_new_config_creates_file_and_supports_force_overwrite() { + let _lock = acquire_lock(); + let temp = TempDir::new().expect("temp dir"); + let path = temp.path().join("configs").join("demo.json"); + + generate_new_config(&path, vec![1], false) + .await + .expect("initial create succeeds"); + assert!(path.exists()); + + let parsed: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(&path).expect("read config")) + .expect("parse generated config"); + assert!(parsed["networks"]["1"].is_object()); + + let exists_err = generate_new_config(&path, vec![1], false) + .await + .unwrap_err(); + assert!(matches!(exists_err, Error::ConfigExists(_))); + + generate_new_config(&path, vec![1, 10], true) + .await + .expect("force overwrite succeeds"); + let overwritten: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(&path).expect("read overwritten config")) + .expect("parse overwritten config"); + assert!(overwritten["networks"]["10"].is_object()); + } + + #[tokio::test] + #[allow(clippy::await_holding_lock)] + async fn load_config_persists_session_with_chains_and_contracts() { + let _lock = acquire_lock(); + let temp = TempDir::new().expect("temp dir"); + let _cwd = CwdGuard::change_to(temp.path()); + + let path = temp.path().join("demo.json"); + generate_new_config(&path, vec![1, 8453], false) + .await + .expect("generate config"); + + load_config(&path, true).await.expect("load config"); + + let storage_root = Path::new(".").join(".oif-demo"); + let storage = Storage::new(&storage_root).expect("storage"); + let session = SessionStore::load(storage).expect("session load"); + + assert_eq!(session.environment(), Environment::Local); + let mut chains = session.chains(); + chains.sort_by_key(|chain| chain.id()); + assert_eq!( + chains.iter().map(|chain| chain.id()).collect::>(), + vec![1, 8453] + ); + + let contracts = session + .contracts(crate::types::chain::ChainId::from_u64(1)) + .expect("contracts for chain 1"); + assert!(contracts.input_settler.is_some()); + assert!(contracts.output_settler.is_some()); + assert!(contracts.permit2.is_some()); + assert!(contracts.input_oracle.is_some()); + assert!(contracts.output_oracle.is_some()); + assert_eq!(contracts.tokens.len(), 2); + assert!(contracts.tokens.contains_key("TOKA")); + assert!(contracts.tokens.contains_key("TOKB")); + } + + #[tokio::test] + #[allow(clippy::await_holding_lock)] + async fn load_config_returns_error_for_invalid_token_address() { + let _lock = acquire_lock(); + let temp = TempDir::new().expect("temp dir"); + let _cwd = CwdGuard::change_to(temp.path()); + + let path = temp.path().join("demo.json"); + generate_new_config(&path, vec![1], false) + .await + .expect("generate config"); + + let mut config_json: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(&path).expect("read config")) + .expect("parse config"); + config_json["networks"]["1"]["tokens"][0]["address"] = json!("not-an-address"); + std::fs::write( + &path, + serde_json::to_string_pretty(&config_json).expect("serialize config"), + ) + .expect("write invalid config"); + + let err = load_config(&path, false).await.unwrap_err(); + match err { + Error::InvalidConfig(message) => { + assert!( + message.to_lowercase().contains("address"), + "unexpected error message: {message}" + ); + }, + other => panic!("expected InvalidConfig, got {other:?}"), + } + } + + fn test_seed_overrides(solver_id: &str) -> solver_types::SeedOverrides { + use alloy_primitives::Address; + use solver_types::{NetworkOverride, SeedOverrides, Token}; + use std::str::FromStr; + + SeedOverrides { + solver_id: Some(solver_id.to_string()), + solver_name: None, + networks: vec![ + NetworkOverride { + chain_id: 11155420, + name: None, + network_type: None, + tokens: vec![Token { + symbol: "USDC".to_string(), + name: None, + address: Address::from_str("0x191688B2Ff5Be8F0A5BCAB3E819C900a810FAaf6") + .expect("valid token address"), + decimals: 6, + }], + rpc_urls: None, + input_settler_address: None, + output_settler_address: None, + input_settler_compact_address: None, + the_compact_address: None, + allocator_address: None, + }, + NetworkOverride { + chain_id: 84532, + name: None, + network_type: None, + tokens: vec![Token { + symbol: "USDC".to_string(), + name: None, + address: Address::from_str("0x73c83DAcc74bB8a704717AC09703b959E74b9705") + .expect("valid token address"), + decimals: 6, + }], + rpc_urls: None, + input_settler_address: None, + output_settler_address: None, + input_settler_compact_address: None, + the_compact_address: None, + allocator_address: None, + }, + ], + settlement: None, + routing_defaults: None, + account: None, + admin: None, + auth_enabled: None, + min_profitability_pct: None, + gas_buffer_bps: None, + commission_bps: None, + rate_buffer_bps: None, + } + } + + #[test] + fn resolve_solver_id_prefers_explicit_value() { + let _lock = acquire_lock(); + let _guard = EnvVarGuard::set("SOLVER_ID", Some("from-env")); + let resolved = + resolve_solver_id(Some("explicit-solver".to_string())).expect("should resolve"); + assert_eq!(resolved, "explicit-solver"); + } + + #[test] + fn resolve_solver_id_reads_env_fallback() { + let _lock = acquire_lock(); + let _guard = EnvVarGuard::set("SOLVER_ID", Some("from-env")); + let resolved = resolve_solver_id(None).expect("should resolve from env"); + assert_eq!(resolved, "from-env"); + } + + #[test] + fn resolve_solver_id_errors_when_missing() { + let _lock = acquire_lock(); + let _guard = EnvVarGuard::set("SOLVER_ID", None); + let err = resolve_solver_id(None).expect_err("missing solver id should error"); + let msg = err.to_string(); + assert!(msg.contains("Solver ID not provided")); + } + + #[tokio::test] + #[allow(clippy::await_holding_lock)] + async fn load_config_from_storage_file_backend_materializes_and_loads_session() { + use solver_service::{config_merge::merge_to_operator_config, seeds::SeedPreset}; + use solver_storage::StoreConfig; + + let _lock = acquire_lock(); + let temp_dir = TempDir::new().expect("temp dir"); + let _cwd_guard = CwdGuard::change_to(temp_dir.path()); + + let store_path = temp_dir.path().join("store"); + std::fs::create_dir_all(&store_path).expect("store dir"); + + let solver_id = "demo-storage-solver"; + let seed = SeedPreset::Testnet.get_seed(); + let operator_config = merge_to_operator_config(test_seed_overrides(solver_id), seed) + .expect("valid operator config"); + + let store = create_config_store::( + StoreConfig::File { + path: store_path.to_string_lossy().to_string(), + }, + operator_config_store_key(solver_id), + ) + .expect("create config store"); + store + .seed(operator_config) + .await + .expect("seed operator config"); + + let _backend_guard = EnvVarGuard::set("STORAGE_BACKEND", Some("file")); + let store_path_value = store_path.to_string_lossy().to_string(); + let _path_guard = EnvVarGuard::set("STORAGE_PATH", Some(&store_path_value)); + let _solver_guard = EnvVarGuard::set("SOLVER_ID", Some(solver_id)); + + let generated_path = load_config_from_storage(None, false) + .await + .expect("load config from storage"); + + assert!(generated_path.exists(), "generated config should exist"); + assert!(Path::new(".oif-demo/session.json").exists()); + + let loaded = Config::load(&generated_path) + .await + .expect("load generated config"); + assert_eq!(loaded.solver.solver.id, solver_id); + assert!(loaded.chains().len() >= 2); + } } diff --git a/crates/solver-demo/src/types/error.rs b/crates/solver-demo/src/types/error.rs index f83de2bc..256cddf3 100644 --- a/crates/solver-demo/src/types/error.rs +++ b/crates/solver-demo/src/types/error.rs @@ -134,10 +134,6 @@ pub enum Error { #[error("JSON error: {0}")] Json(#[from] serde_json::Error), - // TOML errors - #[error("TOML error: {0}")] - Toml(#[from] toml::de::Error), - // HTTP errors #[error("HTTP error: {0}")] Http(#[from] reqwest::Error), diff --git a/crates/solver-discovery/Cargo.toml b/crates/solver-discovery/Cargo.toml index 16f45e3e..6e289fc5 100644 --- a/crates/solver-discovery/Cargo.toml +++ b/crates/solver-discovery/Cargo.toml @@ -24,7 +24,6 @@ serde_json = "1.0" solver-types = { path = "../solver-types", features = ["oif-interfaces"] } thiserror = "2.0.17" tokio = { version = "1.0", features = ["sync", "rt-multi-thread"] } -toml = { workspace = true } tower = "0.5" tower-http = { version = "0.6.6", features = ["cors"] } tracing = "0.1" diff --git a/crates/solver-discovery/src/implementations/offchain/_7683.rs b/crates/solver-discovery/src/implementations/offchain/_7683.rs index 977a6d17..eca69df4 100644 --- a/crates/solver-discovery/src/implementations/offchain/_7683.rs +++ b/crates/solver-discovery/src/implementations/offchain/_7683.rs @@ -786,14 +786,16 @@ pub struct Eip7683OffchainDiscoverySchema; impl Eip7683OffchainDiscoverySchema { /// Static validation method for use before instance creation - pub fn validate_config(config: &toml::Value) -> Result<(), solver_types::ValidationError> { + pub fn validate_config( + config: &serde_json::Value, + ) -> Result<(), solver_types::ValidationError> { let instance = Self; instance.validate(config) } } impl ConfigSchema for Eip7683OffchainDiscoverySchema { - fn validate(&self, config: &toml::Value) -> Result<(), solver_types::ValidationError> { + fn validate(&self, config: &serde_json::Value) -> Result<(), solver_types::ValidationError> { let schema = Schema::new( // Required fields vec![ @@ -881,7 +883,7 @@ impl DiscoveryInterface for Eip7683OffchainDiscovery { /// /// # Arguments /// -/// * `config` - TOML configuration value containing service parameters +/// * `config` - JSON configuration value containing service parameters /// * `networks` - Global networks configuration with RPC URLs and settler addresses /// /// # Returns @@ -891,10 +893,12 @@ impl DiscoveryInterface for Eip7683OffchainDiscovery { /// # Configuration /// /// Expected configuration format: -/// ```toml -/// api_host = "0.0.0.0" # optional, defaults to "0.0.0.0" -/// api_port = 8081 # optional, defaults to 8081 -/// network_ids = [1, 10, 137] # optional, defaults to all networks +/// ```json +/// { +/// "api_host": "0.0.0.0", +/// "api_port": 8081, +/// "network_ids": [1, 10, 137] +/// } /// ``` /// /// # Errors @@ -903,7 +907,7 @@ impl DiscoveryInterface for Eip7683OffchainDiscovery { /// - The networks configuration is invalid /// - The discovery service cannot be created pub fn create_discovery( - config: &toml::Value, + config: &serde_json::Value, networks: &NetworksConfig, ) -> Result, DiscoveryError> { // Validate configuration first @@ -918,7 +922,7 @@ pub fn create_discovery( let api_port = config .get("api_port") - .and_then(|v| v.as_integer()) + .and_then(|v| v.as_i64()) .unwrap_or(8081) as u16; // Get network_ids from config, or default to all networks @@ -927,7 +931,7 @@ pub fn create_discovery( .and_then(|v| v.as_array()) .map(|arr| { arr.iter() - .filter_map(|v| v.as_integer().map(|i| i as u64)) + .filter_map(|v| v.as_i64().map(|i| i as u64)) .collect::>() }) .unwrap_or_else(|| networks.keys().cloned().collect()); @@ -1113,16 +1117,16 @@ mod tests { #[test] fn test_config_schema_validation_success() { - let config = toml::Value::Table({ - let mut table = toml::value::Table::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "api_host".to_string(), - toml::Value::String("127.0.0.1".to_string()), + serde_json::Value::String("127.0.0.1".to_string()), ); - table.insert("api_port".to_string(), toml::Value::Integer(8080)); + table.insert("api_port".to_string(), serde_json::Value::from(8080)); table.insert( "network_ids".to_string(), - toml::Value::Array(vec![toml::Value::Integer(1)]), + serde_json::Value::Array(vec![serde_json::Value::from(1)]), ); table }); @@ -1133,11 +1137,11 @@ mod tests { #[test] fn test_config_schema_validation_missing_required() { - let config = toml::Value::Table({ - let mut table = toml::value::Table::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "api_host".to_string(), - toml::Value::String("127.0.0.1".to_string()), + serde_json::Value::String("127.0.0.1".to_string()), ); // Missing api_port and network_ids table @@ -1149,16 +1153,16 @@ mod tests { #[test] fn test_config_schema_validation_invalid_port() { - let config = toml::Value::Table({ - let mut table = toml::value::Table::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "api_host".to_string(), - toml::Value::String("127.0.0.1".to_string()), + serde_json::Value::String("127.0.0.1".to_string()), ); - table.insert("api_port".to_string(), toml::Value::Integer(70000)); // Invalid port > 65535 + table.insert("api_port".to_string(), serde_json::Value::from(70000)); // Invalid port > 65535 table.insert( "network_ids".to_string(), - toml::Value::Array(vec![toml::Value::Integer(1)]), + serde_json::Value::Array(vec![serde_json::Value::from(1)]), ); table }); @@ -1169,16 +1173,16 @@ mod tests { #[test] fn test_create_discovery_factory_success() { - let config = toml::Value::Table({ - let mut table = toml::value::Table::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "api_host".to_string(), - toml::Value::String("127.0.0.1".to_string()), + serde_json::Value::String("127.0.0.1".to_string()), ); - table.insert("api_port".to_string(), toml::Value::Integer(8080)); + table.insert("api_port".to_string(), serde_json::Value::from(8080)); table.insert( "network_ids".to_string(), - toml::Value::Array(vec![toml::Value::Integer(1)]), + serde_json::Value::Array(vec![serde_json::Value::from(1)]), ); table }); @@ -1190,17 +1194,17 @@ mod tests { #[test] fn test_create_discovery_factory_defaults() { - let config = toml::Value::Table({ - let mut table = toml::value::Table::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); // Provide required fields but use values that will trigger defaults table.insert( "api_host".to_string(), - toml::Value::String("0.0.0.0".to_string()), + serde_json::Value::String("0.0.0.0".to_string()), ); - table.insert("api_port".to_string(), toml::Value::Integer(8081)); + table.insert("api_port".to_string(), serde_json::Value::from(8081)); table.insert( "network_ids".to_string(), - toml::Value::Array(vec![toml::Value::Integer(1)]), + serde_json::Value::Array(vec![serde_json::Value::from(1)]), ); // Don't include auth_token to test that default (None) works table diff --git a/crates/solver-discovery/src/implementations/onchain/_7683.rs b/crates/solver-discovery/src/implementations/onchain/_7683.rs index 2e847939..7ebd3b71 100644 --- a/crates/solver-discovery/src/implementations/onchain/_7683.rs +++ b/crates/solver-discovery/src/implementations/onchain/_7683.rs @@ -423,14 +423,16 @@ pub struct Eip7683DiscoverySchema; impl Eip7683DiscoverySchema { /// Static validation method for use before instance creation - pub fn validate_config(config: &toml::Value) -> Result<(), solver_types::ValidationError> { + pub fn validate_config( + config: &serde_json::Value, + ) -> Result<(), solver_types::ValidationError> { let instance = Self; instance.validate(config) } } impl ConfigSchema for Eip7683DiscoverySchema { - fn validate(&self, config: &toml::Value) -> Result<(), solver_types::ValidationError> { + fn validate(&self, config: &serde_json::Value) -> Result<(), solver_types::ValidationError> { let schema = Schema::new( // Required fields vec![Field::new( @@ -571,7 +573,7 @@ impl DiscoveryInterface for Eip7683Discovery { /// - Any network_id is not found in the networks configuration /// - The discovery service cannot be created (e.g., connection failure) pub fn create_discovery( - config: &toml::Value, + config: &serde_json::Value, networks: &NetworksConfig, ) -> Result, DiscoveryError> { // Validate configuration first @@ -584,7 +586,7 @@ pub fn create_discovery( .and_then(|v| v.as_array()) .map(|arr| { arr.iter() - .filter_map(|v| v.as_integer().map(|i| i as u64)) + .filter_map(|v| v.as_i64().map(|i| i as u64)) .collect::>() }) .ok_or_else(|| DiscoveryError::ValidationError("network_ids is required".to_string()))?; @@ -597,7 +599,7 @@ pub fn create_discovery( let polling_interval_secs = config .get("polling_interval_secs") - .and_then(|v| v.as_integer()) + .and_then(|v| v.as_i64()) .map(|v| v as u64); // Create discovery service synchronously @@ -631,7 +633,6 @@ mod tests { use alloy_rpc_types::Log; use solver_types::utils::tests::builders::{NetworkConfigBuilder, NetworksConfigBuilder}; use solver_types::NetworksConfig; - use std::collections::HashMap; use tokio::sync::mpsc; // Helper function to create a test networks config @@ -698,14 +699,10 @@ mod tests { #[test] fn test_config_schema_validation_valid() { - let config = toml::Value::try_from(HashMap::from([ - ( - "network_ids", - toml::Value::Array(vec![toml::Value::Integer(1)]), - ), - ("polling_interval_secs", toml::Value::Integer(5)), - ])) - .unwrap(); + let config = serde_json::json!({ + "network_ids": [1], + "polling_interval_secs": 5 + }); let result = Eip7683DiscoverySchema::validate_config(&config); assert!(result.is_ok()); @@ -713,11 +710,9 @@ mod tests { #[test] fn test_config_schema_validation_missing_network_ids() { - let config = toml::Value::try_from(HashMap::from([( - "polling_interval_secs", - toml::Value::Integer(5), - )])) - .unwrap(); + let config = serde_json::json!({ + "polling_interval_secs": 5 + }); let result = Eip7683DiscoverySchema::validate_config(&config); assert!(result.is_err()); @@ -725,9 +720,9 @@ mod tests { #[test] fn test_config_schema_validation_empty_network_ids() { - let config = - toml::Value::try_from(HashMap::from([("network_ids", toml::Value::Array(vec![]))])) - .unwrap(); + let config = serde_json::json!({ + "network_ids": [] + }); let result = Eip7683DiscoverySchema::validate_config(&config); assert!(result.is_err()); @@ -735,17 +730,10 @@ mod tests { #[test] fn test_config_schema_validation_invalid_polling_interval() { - let config = toml::Value::try_from(HashMap::from([ - ( - "network_ids", - toml::Value::Array(vec![toml::Value::Integer(1)]), - ), - ( - "polling_interval_secs", - toml::Value::Integer((MAX_POLLING_INTERVAL_SECS + 100) as i64), - ), - ])) - .unwrap(); + let config = serde_json::json!({ + "network_ids": [1], + "polling_interval_secs": (MAX_POLLING_INTERVAL_SECS + 100) + }); let result = Eip7683DiscoverySchema::validate_config(&config); assert!(result.is_err()); @@ -753,14 +741,10 @@ mod tests { #[test] fn test_config_schema_validation_websocket_mode() { - let config = toml::Value::try_from(HashMap::from([ - ( - "network_ids", - toml::Value::Array(vec![toml::Value::Integer(1)]), - ), - ("polling_interval_secs", toml::Value::Integer(0)), // WebSocket mode - ])) - .unwrap(); + let config = serde_json::json!({ + "network_ids": [1], + "polling_interval_secs": 0 + }); // WebSocket mode let result = Eip7683DiscoverySchema::validate_config(&config); assert!(result.is_ok()); @@ -954,11 +938,9 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn test_create_discovery_invalid_config() { - let config = toml::Value::try_from(HashMap::from([ - ("polling_interval_secs", toml::Value::Integer(5)), - // Missing network_ids - ])) - .unwrap(); + let config = serde_json::json!({ + "polling_interval_secs": 5 + }); // Missing network_ids let networks = create_test_networks(); let result = create_discovery(&config, &networks); @@ -973,9 +955,9 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn test_create_discovery_empty_network_ids() { - let config = - toml::Value::try_from(HashMap::from([("network_ids", toml::Value::Array(vec![]))])) - .unwrap(); + let config = serde_json::json!({ + "network_ids": [] + }); let networks = create_test_networks(); let result = create_discovery(&config, &networks); diff --git a/crates/solver-discovery/src/lib.rs b/crates/solver-discovery/src/lib.rs index bda35da8..24df43d8 100644 --- a/crates/solver-discovery/src/lib.rs +++ b/crates/solver-discovery/src/lib.rs @@ -84,7 +84,7 @@ pub trait DiscoveryInterface: Send + Sync { /// This is the function signature that all discovery implementations must provide /// to create instances of their discovery interface. pub type DiscoveryFactory = - fn(&toml::Value, &NetworksConfig) -> Result, DiscoveryError>; + fn(&serde_json::Value, &NetworksConfig) -> Result, DiscoveryError>; /// Registry trait for discovery implementations. /// diff --git a/crates/solver-order/Cargo.toml b/crates/solver-order/Cargo.toml index 4b766b06..bd88f8c2 100644 --- a/crates/solver-order/Cargo.toml +++ b/crates/solver-order/Cargo.toml @@ -16,7 +16,6 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" solver-types = { path = "../solver-types" } thiserror = "2.0" -toml = { workspace = true } tracing = "0.1" uuid = { version = "1.8", features = ["v4", "serde"] } diff --git a/crates/solver-order/src/implementations/standards/_7683.rs b/crates/solver-order/src/implementations/standards/_7683.rs index 1382b2f7..c89c7978 100644 --- a/crates/solver-order/src/implementations/standards/_7683.rs +++ b/crates/solver-order/src/implementations/standards/_7683.rs @@ -149,22 +149,26 @@ impl Eip7683OrderImpl { /// /// # Required Configuration /// -/// ```toml -/// output_settler_address = "0x..." # 42-char hex address -/// input_settler_address = "0x..." # 42-char hex address +/// ```json +/// { +/// "output_settler_address": "0x...", +/// "input_settler_address": "0x..." +/// } /// ``` pub struct Eip7683OrderSchema; impl Eip7683OrderSchema { /// Static validation method for use before instance creation - pub fn validate_config(config: &toml::Value) -> Result<(), solver_types::ValidationError> { + pub fn validate_config( + config: &serde_json::Value, + ) -> Result<(), solver_types::ValidationError> { let instance = Self; instance.validate(config) } } impl ConfigSchema for Eip7683OrderSchema { - fn validate(&self, config: &toml::Value) -> Result<(), solver_types::ValidationError> { + fn validate(&self, config: &serde_json::Value) -> Result<(), solver_types::ValidationError> { let schema = Schema::new( // Required fields vec![], @@ -811,7 +815,7 @@ impl OrderInterface for Eip7683OrderImpl { /// /// Returns an error if networks configuration is invalid. pub fn create_order_impl( - config: &toml::Value, + config: &serde_json::Value, networks: &NetworksConfig, oracle_routes: &solver_types::oracle::OracleRoutes, ) -> Result, OrderError> { @@ -1134,7 +1138,7 @@ mod tests { #[test] fn test_config_schema_validation() { let schema = Eip7683OrderSchema; - let valid_config = toml::Value::Table(toml::map::Map::new()); + let valid_config = serde_json::Value::Object(serde_json::Map::new()); let result = schema.validate(&valid_config); assert!(result.is_ok()); @@ -1142,7 +1146,7 @@ mod tests { #[test] fn test_create_order_impl_factory() { - let config = toml::Value::Table(toml::map::Map::new()); + let config = serde_json::Value::Object(serde_json::Map::new()); let networks = create_test_networks(); let oracle_routes = create_test_oracle_routes(); diff --git a/crates/solver-order/src/implementations/strategies/simple.rs b/crates/solver-order/src/implementations/strategies/simple.rs index 7cefba8f..517e76a5 100644 --- a/crates/solver-order/src/implementations/strategies/simple.rs +++ b/crates/solver-order/src/implementations/strategies/simple.rs @@ -36,7 +36,7 @@ impl SimpleStrategy { pub struct SimpleStrategySchema; impl ConfigSchema for SimpleStrategySchema { - fn validate(&self, config: &toml::Value) -> Result<(), solver_types::ValidationError> { + fn validate(&self, config: &serde_json::Value) -> Result<(), solver_types::ValidationError> { let schema = Schema::new( // Required fields vec![], @@ -176,7 +176,9 @@ impl ExecutionStrategy for SimpleStrategy { /// /// Configuration parameters: /// - `max_gas_price_gwei`: Maximum gas price in gwei (default: 100) -pub fn create_strategy(config: &toml::Value) -> Result, StrategyError> { +pub fn create_strategy( + config: &serde_json::Value, +) -> Result, StrategyError> { // Validate configuration using the schema let schema = SimpleStrategySchema; schema @@ -185,7 +187,7 @@ pub fn create_strategy(config: &toml::Value) -> Result Result, OrderError>; @@ -173,7 +173,8 @@ pub type OrderFactory = fn( /// /// This is the function signature that all strategy implementations must provide /// to create instances of their execution strategy. -pub type StrategyFactory = fn(&toml::Value) -> Result, StrategyError>; +pub type StrategyFactory = + fn(&serde_json::Value) -> Result, StrategyError>; /// Registry trait for order implementations. /// diff --git a/crates/solver-pricing/Cargo.toml b/crates/solver-pricing/Cargo.toml index 9558d1fe..bda58bea 100644 --- a/crates/solver-pricing/Cargo.toml +++ b/crates/solver-pricing/Cargo.toml @@ -15,7 +15,6 @@ serde_json = "1.0" solver-types = { path = "../solver-types" } thiserror = "2.0" tokio = { version = "1.0", features = ["sync", "time"] } -toml = { workspace = true } tracing = "0.1" diff --git a/crates/solver-pricing/src/implementations/coingecko.rs b/crates/solver-pricing/src/implementations/coingecko.rs index 1d635a65..8dfb4b04 100644 --- a/crates/solver-pricing/src/implementations/coingecko.rs +++ b/crates/solver-pricing/src/implementations/coingecko.rs @@ -61,7 +61,7 @@ struct SimplePriceResponse { impl CoinGeckoPricing { /// Creates a new CoinGeckoPricing instance with configuration. - pub fn new(config: &toml::Value) -> Result { + pub fn new(config: &serde_json::Value) -> Result { // Validate configuration first let schema = CoinGeckoConfigSchema; schema.validate(config).map_err(|e| { @@ -86,12 +86,12 @@ impl CoinGeckoPricing { let cache_duration = config .get("cache_duration_seconds") - .and_then(|v| v.as_integer()) + .and_then(|v| v.as_i64()) .unwrap_or(60) as u64; // Default 60 seconds cache let rate_limit_delay_ms = config .get("rate_limit_delay_ms") - .and_then(|v| v.as_integer()) + .and_then(|v| v.as_i64()) .unwrap_or(if api_key.is_some() { 100 } else { 1200 }) as u64; // Pro: 100ms, Free: 1.2s // Build token ID map from shared defaults @@ -101,7 +101,7 @@ impl CoinGeckoPricing { .collect(); // Allow custom token mappings - if let Some(custom_tokens) = config.get("token_id_map").and_then(|v| v.as_table()) { + if let Some(custom_tokens) = config.get("token_id_map").and_then(|v| v.as_object()) { for (symbol, id) in custom_tokens { if let Some(id_str) = id.as_str() { token_id_map.insert(symbol.to_uppercase(), id_str.to_string()); @@ -111,17 +111,17 @@ impl CoinGeckoPricing { // Parse custom prices for tokens (useful for testing/demo) let mut custom_prices = HashMap::new(); - if let Some(prices_config) = config.get("custom_prices").and_then(|v| v.as_table()) { + if let Some(prices_config) = config.get("custom_prices").and_then(|v| v.as_object()) { for (symbol, price) in prices_config { let price_decimal = if let Some(price_str) = price.as_str() { price_str.parse::().map_err(|e| { PricingError::InvalidData(format!("Invalid custom price for {symbol}: {e}")) })? - } else if let Some(price_num) = price.as_float() { + } else if let Some(price_num) = price.as_f64() { Decimal::try_from(price_num).map_err(|e| { PricingError::InvalidData(format!("Invalid custom price for {symbol}: {e}")) })? - } else if let Some(price_int) = price.as_integer() { + } else if let Some(price_int) = price.as_i64() { Decimal::from(price_int) } else { return Err(PricingError::InvalidData(format!( @@ -499,7 +499,7 @@ impl PricingInterface for CoinGeckoPricing { pub struct CoinGeckoConfigSchema; impl ConfigSchema for CoinGeckoConfigSchema { - fn validate(&self, config: &toml::Value) -> Result<(), ValidationError> { + fn validate(&self, config: &serde_json::Value) -> Result<(), ValidationError> { // Optional API key if let Some(api_key) = config.get("api_key") { if api_key.as_str().is_none() { @@ -524,7 +524,7 @@ impl ConfigSchema for CoinGeckoConfigSchema { // Optional cache duration if let Some(cache_duration) = config.get("cache_duration_seconds") { - if cache_duration.as_integer().is_none() { + if cache_duration.as_i64().is_none() { return Err(ValidationError::TypeMismatch { field: "cache_duration_seconds".to_string(), expected: "integer".to_string(), @@ -535,7 +535,7 @@ impl ConfigSchema for CoinGeckoConfigSchema { // Optional rate limit delay if let Some(delay) = config.get("rate_limit_delay_ms") { - if delay.as_integer().is_none() { + if delay.as_i64().is_none() { return Err(ValidationError::TypeMismatch { field: "rate_limit_delay_ms".to_string(), expected: "integer".to_string(), @@ -546,7 +546,7 @@ impl ConfigSchema for CoinGeckoConfigSchema { // Optional token_id_map if let Some(token_map) = config.get("token_id_map") { - if let Some(table) = token_map.as_table() { + if let Some(table) = token_map.as_object() { for (symbol, id) in table { if id.as_str().is_none() { return Err(ValidationError::TypeMismatch { @@ -567,15 +567,15 @@ impl ConfigSchema for CoinGeckoConfigSchema { // Optional custom_prices if let Some(custom_prices) = config.get("custom_prices") { - if let Some(table) = custom_prices.as_table() { + if let Some(table) = custom_prices.as_object() { for (symbol, price) in table { // Price can be string (parseable as Decimal), float, or integer let is_valid = if let Some(price_str) = price.as_str() { price_str.parse::().is_ok() - } else if let Some(price_num) = price.as_float() { + } else if let Some(price_num) = price.as_f64() { Decimal::try_from(price_num).is_ok() } else { - price.as_integer().is_some() + price.as_i64().is_some() }; if !is_valid { @@ -615,7 +615,7 @@ impl PricingRegistry for CoinGeckoPricingRegistry {} /// Factory function for creating CoinGeckoPricing instances. pub fn create_coingecko_pricing( - config: &toml::Value, + config: &serde_json::Value, ) -> Result, PricingError> { Ok(Box::new(CoinGeckoPricing::new(config)?)) } @@ -625,20 +625,18 @@ mod tests { use super::*; use tokio; - fn minimal_config() -> toml::Value { - toml::Value::Table(toml::map::Map::new()) + fn minimal_config() -> serde_json::Value { + serde_json::Value::Object(serde_json::Map::new()) } - fn config_with_custom_prices() -> toml::Value { - toml::from_str( - r#" - cache_duration_seconds = 10 - [custom_prices] - ETH = "3000.50" - BTC = "65000" - "#, - ) - .unwrap() + fn config_with_custom_prices() -> serde_json::Value { + serde_json::json!({ + "cache_duration_seconds": 10, + "custom_prices": { + "ETH": "3000.50", + "BTC": "65000" + } + }) } #[test] @@ -651,7 +649,9 @@ mod tests { #[test] fn test_new_with_api_key() { - let config = toml::from_str(r#"api_key = "pro_key_123""#).unwrap(); + let config = serde_json::json!({ + "api_key": "pro_key_123" + }); let pricing = CoinGeckoPricing::new(&config).unwrap(); assert_eq!(pricing.base_url, "https://pro-api.coingecko.com/api/v3"); assert_eq!(pricing.rate_limit_delay_ms, 100); @@ -766,20 +766,20 @@ mod tests { #[test] fn test_config_schema_invalid_api_key() { let schema = CoinGeckoConfigSchema; - let bad_config = toml::from_str(r#"api_key = 123"#).unwrap(); + let bad_config = serde_json::json!({ + "api_key": 123 + }); assert!(schema.validate(&bad_config).is_err()); } #[test] fn test_config_schema_invalid_custom_price() { let schema = CoinGeckoConfigSchema; - let bad_config = toml::from_str( - r#" - [custom_prices] - ETH = "not_a_price" - "#, - ) - .unwrap(); + let bad_config = serde_json::json!({ + "custom_prices": { + "ETH": "not_a_price" + } + }); assert!(schema.validate(&bad_config).is_err()); } diff --git a/crates/solver-pricing/src/implementations/defillama.rs b/crates/solver-pricing/src/implementations/defillama.rs index e7168a97..fba132c6 100644 --- a/crates/solver-pricing/src/implementations/defillama.rs +++ b/crates/solver-pricing/src/implementations/defillama.rs @@ -64,7 +64,7 @@ struct CoinPrice { impl DefiLlamaPricing { /// Creates a new DefiLlamaPricing instance with configuration. - pub fn new(config: &toml::Value) -> Result { + pub fn new(config: &serde_json::Value) -> Result { // Validate configuration first let schema = DefiLlamaConfigSchema; schema.validate(config).map_err(|e| { @@ -79,7 +79,7 @@ impl DefiLlamaPricing { let cache_duration = config .get("cache_duration_seconds") - .and_then(|v| v.as_integer()) + .and_then(|v| v.as_i64()) .unwrap_or(60) as u64; // Build token ID map from shared defaults, adding "coingecko:" prefix for DeFiLlama API @@ -89,7 +89,7 @@ impl DefiLlamaPricing { .collect(); // Allow custom token mappings - if let Some(custom_tokens) = config.get("token_id_map").and_then(|v| v.as_table()) { + if let Some(custom_tokens) = config.get("token_id_map").and_then(|v| v.as_object()) { for (symbol, id) in custom_tokens { if let Some(id_str) = id.as_str() { token_id_map.insert(symbol.to_uppercase(), id_str.to_string()); @@ -99,17 +99,17 @@ impl DefiLlamaPricing { // Parse custom prices for tokens (useful for testing/demo) let mut custom_prices = HashMap::new(); - if let Some(prices_config) = config.get("custom_prices").and_then(|v| v.as_table()) { + if let Some(prices_config) = config.get("custom_prices").and_then(|v| v.as_object()) { for (symbol, price) in prices_config { let price_decimal = if let Some(price_str) = price.as_str() { price_str.parse::().map_err(|e| { PricingError::InvalidData(format!("Invalid custom price for {symbol}: {e}")) })? - } else if let Some(price_num) = price.as_float() { + } else if let Some(price_num) = price.as_f64() { Decimal::try_from(price_num).map_err(|e| { PricingError::InvalidData(format!("Invalid custom price for {symbol}: {e}")) })? - } else if let Some(price_int) = price.as_integer() { + } else if let Some(price_int) = price.as_i64() { Decimal::from(price_int) } else { return Err(PricingError::InvalidData(format!( @@ -441,7 +441,7 @@ impl PricingInterface for DefiLlamaPricing { pub struct DefiLlamaConfigSchema; impl ConfigSchema for DefiLlamaConfigSchema { - fn validate(&self, config: &toml::Value) -> Result<(), ValidationError> { + fn validate(&self, config: &serde_json::Value) -> Result<(), ValidationError> { // Optional base URL if let Some(base_url) = config.get("base_url") { if base_url.as_str().is_none() { @@ -455,7 +455,7 @@ impl ConfigSchema for DefiLlamaConfigSchema { // Optional cache duration if let Some(cache_duration) = config.get("cache_duration_seconds") { - if cache_duration.as_integer().is_none() { + if cache_duration.as_i64().is_none() { return Err(ValidationError::TypeMismatch { field: "cache_duration_seconds".to_string(), expected: "integer".to_string(), @@ -466,7 +466,7 @@ impl ConfigSchema for DefiLlamaConfigSchema { // Optional token_id_map if let Some(token_map) = config.get("token_id_map") { - if let Some(table) = token_map.as_table() { + if let Some(table) = token_map.as_object() { for (symbol, id) in table { if id.as_str().is_none() { return Err(ValidationError::TypeMismatch { @@ -487,14 +487,14 @@ impl ConfigSchema for DefiLlamaConfigSchema { // Optional custom_prices if let Some(custom_prices) = config.get("custom_prices") { - if let Some(table) = custom_prices.as_table() { + if let Some(table) = custom_prices.as_object() { for (symbol, price) in table { let is_valid = if let Some(price_str) = price.as_str() { price_str.parse::().is_ok() - } else if let Some(price_num) = price.as_float() { + } else if let Some(price_num) = price.as_f64() { Decimal::try_from(price_num).is_ok() } else { - price.as_integer().is_some() + price.as_i64().is_some() }; if !is_valid { @@ -534,7 +534,7 @@ impl PricingRegistry for DefiLlamaPricingRegistry {} /// Factory function for creating DefiLlamaPricing instances. pub fn create_defillama_pricing( - config: &toml::Value, + config: &serde_json::Value, ) -> Result, PricingError> { Ok(Box::new(DefiLlamaPricing::new(config)?)) } @@ -543,52 +543,43 @@ pub fn create_defillama_pricing( mod tests { use super::*; - fn minimal_config() -> toml::Value { - toml::Value::Table(toml::map::Map::new()) - } - - fn config_with_custom_prices() -> toml::Value { - toml::from_str( - r#" - cache_duration_seconds = 10 - [custom_prices] - ETH = "3000.50" - BTC = "65000" - "#, - ) - .unwrap() - } - - fn config_with_custom_base_url() -> toml::Value { - toml::from_str( - r#" - base_url = "https://custom.llama.fi" - cache_duration_seconds = 120 - "#, - ) - .unwrap() - } - - fn config_with_custom_token_map() -> toml::Value { - toml::from_str( - r#" - [token_id_map] - MYTOKEN = "coingecko:my-custom-token" - CUSTOM = "coingecko:custom-coin" - "#, - ) - .unwrap() - } - - fn config_with_float_custom_prices() -> toml::Value { - toml::from_str( - r#" - [custom_prices] - ETH = 3000.123 - SOL = 150 - "#, - ) - .unwrap() + fn minimal_config() -> serde_json::Value { + serde_json::Value::Object(serde_json::Map::new()) + } + + fn config_with_custom_prices() -> serde_json::Value { + serde_json::json!({ + "cache_duration_seconds": 10, + "custom_prices": { + "ETH": "3000.50", + "BTC": "65000" + } + }) + } + + fn config_with_custom_base_url() -> serde_json::Value { + serde_json::json!({ + "base_url": "https://custom.llama.fi", + "cache_duration_seconds": 120 + }) + } + + fn config_with_custom_token_map() -> serde_json::Value { + serde_json::json!({ + "token_id_map": { + "MYTOKEN": "coingecko:my-custom-token", + "CUSTOM": "coingecko:custom-coin" + } + }) + } + + fn config_with_float_custom_prices() -> serde_json::Value { + serde_json::json!({ + "custom_prices": { + "ETH": 3000.123, + "SOL": 150 + } + }) } #[test] @@ -710,13 +701,11 @@ mod tests { #[test] fn test_config_schema_invalid_custom_price() { let schema = DefiLlamaConfigSchema; - let bad_config = toml::from_str( - r#" - [custom_prices] - ETH = "not_a_price" - "#, - ) - .unwrap(); + let bad_config = serde_json::json!({ + "custom_prices": { + "ETH": "not_a_price" + } + }); assert!(schema.validate(&bad_config).is_err()); } @@ -813,61 +802,47 @@ mod tests { #[test] fn test_config_schema_invalid_base_url_type() { let schema = DefiLlamaConfigSchema; - let bad_config = toml::from_str( - r#" - base_url = 123 - "#, - ) - .unwrap(); + let bad_config = serde_json::json!({ + "base_url": 123 + }); assert!(schema.validate(&bad_config).is_err()); } #[test] fn test_config_schema_invalid_cache_duration_type() { let schema = DefiLlamaConfigSchema; - let bad_config = toml::from_str( - r#" - cache_duration_seconds = "not_a_number" - "#, - ) - .unwrap(); + let bad_config = serde_json::json!({ + "cache_duration_seconds": "not_a_number" + }); assert!(schema.validate(&bad_config).is_err()); } #[test] fn test_config_schema_invalid_token_map_not_table() { let schema = DefiLlamaConfigSchema; - let bad_config = toml::from_str( - r#" - token_id_map = "invalid" - "#, - ) - .unwrap(); + let bad_config = serde_json::json!({ + "token_id_map": "invalid" + }); assert!(schema.validate(&bad_config).is_err()); } #[test] fn test_config_schema_invalid_token_map_value() { let schema = DefiLlamaConfigSchema; - let bad_config = toml::from_str( - r#" - [token_id_map] - MYTOKEN = 123 - "#, - ) - .unwrap(); + let bad_config = serde_json::json!({ + "token_id_map": { + "MYTOKEN": 123 + } + }); assert!(schema.validate(&bad_config).is_err()); } #[test] fn test_config_schema_invalid_custom_prices_not_table() { let schema = DefiLlamaConfigSchema; - let bad_config = toml::from_str( - r#" - custom_prices = "invalid" - "#, - ) - .unwrap(); + let bad_config = serde_json::json!({ + "custom_prices": "invalid" + }); assert!(schema.validate(&bad_config).is_err()); } diff --git a/crates/solver-pricing/src/implementations/mock.rs b/crates/solver-pricing/src/implementations/mock.rs index f511c301..7e2a4975 100644 --- a/crates/solver-pricing/src/implementations/mock.rs +++ b/crates/solver-pricing/src/implementations/mock.rs @@ -13,7 +13,6 @@ use solver_types::{ MOCK_TOKB_USD_PRICE, }; use std::collections::HashMap; -use toml; /// Mock pricing implementation with fixed asset prices. pub struct MockPricing { @@ -23,7 +22,7 @@ pub struct MockPricing { impl MockPricing { /// Creates a new MockPricing instance with configuration. - pub fn new(config: &toml::Value) -> Result { + pub fn new(config: &serde_json::Value) -> Result { let mut pair_prices = HashMap::new(); // Default prices @@ -36,7 +35,7 @@ impl MockPricing { pair_prices.insert("TOKB/USD".to_string(), MOCK_TOKB_USD_PRICE.to_string()); // Allow configuration overrides - if let Some(prices) = config.get("pair_prices").and_then(|v| v.as_table()) { + if let Some(prices) = config.get("pair_prices").and_then(|v| v.as_object()) { for (pair, price) in prices { if let Some(price_str) = price.as_str() { pair_prices.insert(pair.to_uppercase(), price_str.to_string()); @@ -212,10 +211,10 @@ impl PricingInterface for MockPricing { pub struct MockPricingSchema; impl ConfigSchema for MockPricingSchema { - fn validate(&self, config: &toml::Value) -> Result<(), ValidationError> { + fn validate(&self, config: &serde_json::Value) -> Result<(), ValidationError> { // Optional pair_prices validation if let Some(pair_prices) = config.get("pair_prices") { - if let Some(table) = pair_prices.as_table() { + if let Some(table) = pair_prices.as_object() { for (pair, price) in table { // Validate pair format if !pair.contains('/') { @@ -263,7 +262,7 @@ impl PricingRegistry for MockPricingRegistry {} /// Factory function for creating MockPricing instances. pub fn create_mock_pricing( - config: &toml::Value, + config: &serde_json::Value, ) -> Result, PricingError> { Ok(Box::new(MockPricing::new(config)?)) } @@ -272,8 +271,8 @@ pub fn create_mock_pricing( mod tests { use super::*; - fn create_default_config() -> toml::Value { - toml::Value::Table(toml::map::Map::new()) + fn create_default_config() -> serde_json::Value { + serde_json::Value::Object(serde_json::Map::new()) } #[tokio::test] diff --git a/crates/solver-pricing/src/lib.rs b/crates/solver-pricing/src/lib.rs index 572d2156..1acb8a5e 100644 --- a/crates/solver-pricing/src/lib.rs +++ b/crates/solver-pricing/src/lib.rs @@ -49,7 +49,7 @@ pub trait PricingInterface: Send + Sync { } /// Type alias for pricing factory functions. -pub type PricingFactory = fn(&toml::Value) -> Result, PricingError>; +pub type PricingFactory = fn(&serde_json::Value) -> Result, PricingError>; /// Registry trait for pricing implementations. pub trait PricingRegistry: ImplementationRegistry {} @@ -122,7 +122,7 @@ impl PricingConfig { } /// Builds pricing config from a TOML table (e.g. strategy implementation table) - pub fn from_table(table: &toml::Value) -> Self { + pub fn from_table(table: &serde_json::Value) -> Self { let defaults = Self::default_values(); Self { currency: table @@ -132,11 +132,11 @@ impl PricingConfig { .to_string(), commission_bps: table .get("commission_bps") - .and_then(|v| v.as_integer()) + .and_then(|v| v.as_i64()) .unwrap_or(defaults.commission_bps as i64) as u32, rate_buffer_bps: table .get("rate_buffer_bps") - .and_then(|v| v.as_integer()) + .and_then(|v| v.as_i64()) .unwrap_or(defaults.rate_buffer_bps as i64) as u32, enable_live_gas_estimate: table .get("enable_live_gas_estimate") @@ -305,7 +305,7 @@ mod tests { #[test] fn test_pricing_config_from_empty_table() { - let table = toml::Value::Table(toml::map::Map::new()); + let table = serde_json::Value::Object(serde_json::Map::new()); let config = PricingConfig::from_table(&table); // Should use all defaults assert_eq!(config.currency, "USD"); @@ -316,15 +316,12 @@ mod tests { #[test] fn test_pricing_config_from_table_with_values() { - let table: toml::Value = toml::from_str( - r#" - pricing_currency = "EUR" - commission_bps = 50 - rate_buffer_bps = 25 - enable_live_gas_estimate = true - "#, - ) - .unwrap(); + let table = serde_json::json!({ + "pricing_currency": "EUR", + "commission_bps": 50, + "rate_buffer_bps": 25, + "enable_live_gas_estimate": true + }); let config = PricingConfig::from_table(&table); assert_eq!(config.currency, "EUR"); assert_eq!(config.commission_bps, 50); @@ -334,13 +331,10 @@ mod tests { #[test] fn test_pricing_config_from_table_partial_values() { - let table: toml::Value = toml::from_str( - r#" - pricing_currency = "GBP" - commission_bps = 30 - "#, - ) - .unwrap(); + let table = serde_json::json!({ + "pricing_currency": "GBP", + "commission_bps": 30 + }); let config = PricingConfig::from_table(&table); assert_eq!(config.currency, "GBP"); assert_eq!(config.commission_bps, 30); @@ -375,7 +369,7 @@ mod tests { use solver_types::ConfigSchema; fn create_test_pricing() -> Box { - let config = toml::Value::Table(toml::map::Map::new()); + let config = serde_json::Value::Object(serde_json::Map::new()); create_mock_pricing(&config).unwrap() } @@ -385,7 +379,10 @@ mod tests { struct FailingConfigSchema; impl ConfigSchema for FailingConfigSchema { - fn validate(&self, _config: &toml::Value) -> Result<(), solver_types::ValidationError> { + fn validate( + &self, + _config: &serde_json::Value, + ) -> Result<(), solver_types::ValidationError> { Ok(()) } } diff --git a/crates/solver-service/Cargo.toml b/crates/solver-service/Cargo.toml index 52664ef0..a4119c02 100644 --- a/crates/solver-service/Cargo.toml +++ b/crates/solver-service/Cargo.toml @@ -48,7 +48,6 @@ solver-types = { path = "../solver-types", features = ["oif-interfaces"] } subtle = "2.6" thiserror = "2.0.17" tokio = { version = "1.0", features = ["full"] } -toml = { workspace = true } tower = "0.5" tower-http = { version = "0.6.6", features = ["cors", "normalize-path"] } tracing = "0.1" diff --git a/crates/solver-service/src/apis/order.rs b/crates/solver-service/src/apis/order.rs index 6065b02f..57c954fb 100644 --- a/crates/solver-service/src/apis/order.rs +++ b/crates/solver-service/src/apis/order.rs @@ -303,6 +303,7 @@ mod tests { use alloy_primitives::U256; use mockall::predicate::eq; use serde_json::json; + use serde_json::Value; use solver_account::{implementations::local::LocalWallet, AccountService}; use solver_config::{Config, ConfigBuilder}; use solver_core::{engine::token_manager::TokenManager, EventBus, SolverEngine}; @@ -315,7 +316,6 @@ mod tests { use solver_types::utils::tests::builders::OrderBuilder; use solver_types::{order::Order, OrderStatus, TransactionHash}; use std::{collections::HashMap, sync::Arc}; - use toml::Value; const TEST_PK: &str = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; const TEST_ADDR: &str = "0x1234567890123456789012345678901234567890"; @@ -342,7 +342,7 @@ mod tests { let providers: HashMap> = HashMap::new(); let delivery = Arc::new(DeliveryService::new(providers, 1, 3)); let discovery = Arc::new(DiscoveryService::new(HashMap::new())); - let strategy = create_strategy(&Value::Table(toml::map::Map::new())).unwrap(); + let strategy = create_strategy(&Value::Object(serde_json::Map::new())).unwrap(); let order = Arc::new(OrderService::new(HashMap::new(), strategy)); let settlement = Arc::new(SettlementService::new(HashMap::new(), 3)); let event_bus = EventBus::new(64); @@ -355,7 +355,7 @@ mod tests { let solver_address = addr(); // Create a mock pricing service for tests - let pricing_config = toml::Value::Table(toml::map::Map::new()); + let pricing_config = serde_json::Value::Object(serde_json::Map::new()); let pricing_impl = mock::create_mock_pricing(&pricing_config).unwrap(); let pricing = Arc::new(PricingService::new(pricing_impl, Vec::new())); diff --git a/crates/solver-service/src/apis/quote/mod.rs b/crates/solver-service/src/apis/quote/mod.rs index 01308751..b8227f59 100644 --- a/crates/solver-service/src/apis/quote/mod.rs +++ b/crates/solver-service/src/apis/quote/mod.rs @@ -289,52 +289,65 @@ mod tests { /// Creates a minimal SolverEngine for testing fn create_test_solver_engine() -> SolverEngine { - // Create minimal config for testing - let config_toml = r#" - [solver] - id = "test-solver" - monitoring_timeout_seconds = 30 - min_profitability_pct = 1.0 - - [storage] - primary = "memory" - cleanup_interval_seconds = 3600 - [storage.implementations.memory] - - [delivery] - min_confirmations = 1 - [delivery.implementations] - - [account] - primary = "local" - [account.implementations.local] - private_key = "0x1234567890123456789012345678901234567890123456789012345678901234" - - [discovery] - [discovery.implementations] - - [order] - [order.implementations] - [order.strategy] - primary = "simple" - [order.strategy.implementations.simple] - - [settlement] - [settlement.implementations] - - [networks.1] - chain_id = 1 - input_settler_address = "0x1111111111111111111111111111111111111111" - output_settler_address = "0x2222222222222222222222222222222222222222" - [[networks.1.rpc_urls]] - http = "http://localhost:8545" - [[networks.1.tokens]] - symbol = "TEST" - address = "0x3333333333333333333333333333333333333333" - decimals = 18 - "#; - let config: solver_config::Config = - toml::from_str(config_toml).expect("Failed to parse test config"); + let config: solver_config::Config = serde_json::from_value(serde_json::json!({ + "solver": { + "id": "test-solver", + "monitoring_timeout_seconds": 30, + "min_profitability_pct": 1.0 + }, + "storage": { + "primary": "memory", + "cleanup_interval_seconds": 3600, + "implementations": { + "memory": {} + } + }, + "delivery": { + "min_confirmations": 1, + "implementations": {} + }, + "account": { + "primary": "local", + "implementations": { + "local": { + "private_key": "0x1234567890123456789012345678901234567890123456789012345678901234" + } + } + }, + "discovery": { + "implementations": {} + }, + "order": { + "implementations": {}, + "strategy": { + "primary": "simple", + "implementations": { + "simple": {} + } + } + }, + "settlement": { + "implementations": {} + }, + "networks": { + "1": { + "chain_id": 1, + "input_settler_address": "0x1111111111111111111111111111111111111111", + "output_settler_address": "0x2222222222222222222222222222222222222222", + "rpc_urls": [ + { "http": "http://localhost:8545" } + ], + "tokens": [ + { + "symbol": "TEST", + "address": "0x3333333333333333333333333333333333333333", + "decimals": 18 + } + ] + } + } + })) + .expect("Failed to parse test config"); // Create mock services let storage = create_test_storage(); @@ -348,13 +361,13 @@ mod tests { let delivery = Arc::new(DeliveryService::new(HashMap::new(), 1, 20)); let discovery = Arc::new(DiscoveryService::new(HashMap::new())); let strategy = solver_order::implementations::strategies::simple::create_strategy( - &toml::Value::Table(toml::map::Map::new()), + &serde_json::Value::Object(serde_json::Map::new()), ) .unwrap(); let order = Arc::new(OrderService::new(HashMap::new(), strategy)); let settlement = Arc::new(SettlementService::new(HashMap::new(), 3)); let pricing_impl = solver_pricing::implementations::mock::create_mock_pricing( - &toml::Value::Table(toml::map::Map::new()), + &serde_json::Value::Object(serde_json::Map::new()), ) .unwrap(); let pricing = Arc::new(PricingService::new(pricing_impl, Vec::new())); diff --git a/crates/solver-service/src/apis/quote/validation.rs b/crates/solver-service/src/apis/quote/validation.rs index 0a71d98f..c211f90f 100644 --- a/crates/solver-service/src/apis/quote/validation.rs +++ b/crates/solver-service/src/apis/quote/validation.rs @@ -720,52 +720,58 @@ mod tests { use solver_storage::StorageService; use solver_types::Address; - // Create minimal config for testing - let config_toml = r#" - [solver] - id = "test-solver" - monitoring_timeout_seconds = 30 - min_profitability_pct = 1.0 - - [storage] - primary = "memory" - cleanup_interval_seconds = 3600 - [storage.implementations.memory] - - [delivery] - min_confirmations = 1 - [delivery.implementations] - - [account] - primary = "local" - [account.implementations.local] - private_key = "0x1234567890123456789012345678901234567890123456789012345678901234" - - [discovery] - [discovery.implementations] - - [order] - [order.implementations] - [order.strategy] - primary = "simple" - [order.strategy.implementations.simple] - - [settlement] - [settlement.implementations] - - [networks] - "#; - let config: Config = toml::from_str(config_toml).expect("Failed to parse test config"); + let config: Config = serde_json::from_value(serde_json::json!({ + "solver": { + "id": "test-solver", + "monitoring_timeout_seconds": 30, + "min_profitability_pct": 1.0 + }, + "storage": { + "primary": "memory", + "cleanup_interval_seconds": 3600, + "implementations": { + "memory": {} + } + }, + "delivery": { + "min_confirmations": 1, + "implementations": {} + }, + "account": { + "primary": "local", + "implementations": { + "local": { + "private_key": "0x1234567890123456789012345678901234567890123456789012345678901234" + } + } + }, + "discovery": { + "implementations": {} + }, + "order": { + "implementations": {}, + "strategy": { + "primary": "simple", + "implementations": { + "simple": {} + } + } + }, + "settlement": { + "implementations": {} + }, + "networks": {} + })) + .expect("Failed to parse test config"); // Create mock services let storage = Arc::new(StorageService::new(Box::new( solver_storage::implementations::memory::MemoryStorage::new(), ))); - let account_config = toml::from_str( - r#"private_key = "0x1234567890123456789012345678901234567890123456789012345678901234""#, - ) - .expect("Failed to parse account config"); + let account_config = serde_json::json!({ + "private_key": "0x1234567890123456789012345678901234567890123456789012345678901234" + }); let account = Arc::new(AccountService::new( solver_account::implementations::local::create_account(&account_config) .await @@ -776,7 +782,7 @@ mod tests { let delivery = Arc::new(DeliveryService::new(HashMap::new(), 1, 20)); let discovery = Arc::new(DiscoveryService::new(HashMap::new())); - let strategy_config = toml::Value::Table(toml::value::Table::new()); + let strategy_config = serde_json::Value::Object(serde_json::Map::new()); let strategy = solver_order::implementations::strategies::simple::create_strategy(&strategy_config) .expect("Failed to create strategy"); @@ -784,7 +790,7 @@ mod tests { let settlement = Arc::new(SettlementService::new(HashMap::new(), 20)); - let pricing_config = toml::Value::Table(toml::value::Table::new()); + let pricing_config = serde_json::Value::Object(serde_json::Map::new()); let pricing_impl = solver_pricing::implementations::mock::create_mock_pricing(&pricing_config) .expect("Failed to create mock pricing"); @@ -2088,45 +2094,51 @@ mod tests { simulate_callbacks: bool, whitelist: Vec, ) -> solver_config::Config { - let config_toml = format!( - r#" - [solver] - id = "test-solver" - monitoring_timeout_seconds = 30 - min_profitability_pct = 1.0 - - [storage] - primary = "memory" - cleanup_interval_seconds = 3600 - [storage.implementations.memory] - - [delivery] - min_confirmations = 1 - [delivery.implementations] - - [account] - primary = "local" - [account.implementations.local] - private_key = "0x1234567890123456789012345678901234567890123456789012345678901234" - - [discovery] - [discovery.implementations] - - [order] - simulate_callbacks = {simulate_callbacks} - callback_whitelist = {whitelist:?} - [order.implementations] - [order.strategy] - primary = "simple" - [order.strategy.implementations.simple] - - [settlement] - [settlement.implementations] - - [networks] - "# - ); - toml::from_str(&config_toml).expect("Failed to parse test config") + serde_json::from_value(serde_json::json!({ + "solver": { + "id": "test-solver", + "monitoring_timeout_seconds": 30, + "min_profitability_pct": 1.0 + }, + "storage": { + "primary": "memory", + "cleanup_interval_seconds": 3600, + "implementations": { + "memory": {} + } + }, + "delivery": { + "min_confirmations": 1, + "implementations": {} + }, + "account": { + "primary": "local", + "implementations": { + "local": { + "private_key": "0x1234567890123456789012345678901234567890123456789012345678901234" + } + } + }, + "discovery": { + "implementations": {} + }, + "order": { + "simulate_callbacks": simulate_callbacks, + "callback_whitelist": whitelist, + "implementations": {}, + "strategy": { + "primary": "simple", + "implementations": { + "simple": {} + } + } + }, + "settlement": { + "implementations": {} + }, + "networks": {} + })) + .expect("Failed to parse test config") } fn create_request_with_callback(calldata: Option<&str>) -> GetQuoteRequest { diff --git a/crates/solver-service/src/apis/tokens.rs b/crates/solver-service/src/apis/tokens.rs index c0d72a03..d6e68545 100644 --- a/crates/solver-service/src/apis/tokens.rs +++ b/crates/solver-service/src/apis/tokens.rs @@ -338,52 +338,65 @@ mod tests { async fn create_mock_solver_engine_with_token_manager( token_manager: Arc, ) -> Arc { - // Create minimal config for testing - let config_toml = r#" - [solver] - id = "test-solver" - monitoring_timeout_seconds = 30 - min_profitability_pct = 1.0 - - [storage] - primary = "memory" - cleanup_interval_seconds = 3600 - [storage.implementations.memory] - - [delivery] - min_confirmations = 1 - [delivery.implementations] - - [account] - primary = "local" - [account.implementations.local] - private_key = "0x1234567890123456789012345678901234567890123456789012345678901234" - - [discovery] - [discovery.implementations] - - [order] - [order.implementations] - [order.strategy] - primary = "simple" - [order.strategy.implementations.simple] - - [settlement] - [settlement.implementations] - - [networks.1] - chain_id = 1 - input_settler_address = "0x1111111111111111111111111111111111111111" - output_settler_address = "0x2222222222222222222222222222222222222222" - [[networks.1.rpc_urls]] - http = "http://localhost:8545" - [[networks.1.tokens]] - symbol = "TEST" - address = "0x3333333333333333333333333333333333333333" - decimals = 18 - "#; - let config: solver_config::Config = - toml::from_str(config_toml).expect("Failed to parse test config"); + let config: solver_config::Config = serde_json::from_value(serde_json::json!({ + "solver": { + "id": "test-solver", + "monitoring_timeout_seconds": 30, + "min_profitability_pct": 1.0 + }, + "storage": { + "primary": "memory", + "cleanup_interval_seconds": 3600, + "implementations": { + "memory": {} + } + }, + "delivery": { + "min_confirmations": 1, + "implementations": {} + }, + "account": { + "primary": "local", + "implementations": { + "local": { + "private_key": "0x1234567890123456789012345678901234567890123456789012345678901234" + } + } + }, + "discovery": { + "implementations": {} + }, + "order": { + "implementations": {}, + "strategy": { + "primary": "simple", + "implementations": { + "simple": {} + } + } + }, + "settlement": { + "implementations": {} + }, + "networks": { + "1": { + "chain_id": 1, + "input_settler_address": "0x1111111111111111111111111111111111111111", + "output_settler_address": "0x2222222222222222222222222222222222222222", + "rpc_urls": [ + { "http": "http://localhost:8545" } + ], + "tokens": [ + { + "symbol": "TEST", + "address": "0x3333333333333333333333333333333333333333", + "decimals": 18 + } + ] + } + } + })) + .expect("Failed to parse test config"); // Create mock services using proper constructors let storage = Arc::new(solver_storage::StorageService::new(Box::new( @@ -391,10 +404,9 @@ mod tests { ))); // Create account service with local wallet - let account_config = toml::from_str( - r#"private_key = "0x1234567890123456789012345678901234567890123456789012345678901234""#, - ) - .expect("Failed to parse account config"); + let account_config = serde_json::json!({ + "private_key": "0x1234567890123456789012345678901234567890123456789012345678901234" + }); let account = Arc::new(solver_account::AccountService::new( solver_account::implementations::local::create_account(&account_config) .await @@ -411,7 +423,7 @@ mod tests { let discovery = Arc::new(solver_discovery::DiscoveryService::new(HashMap::new())); // Create order service - needs implementations and strategy - let strategy_config = toml::Value::Table(toml::value::Table::new()); + let strategy_config = serde_json::Value::Object(serde_json::Map::new()); let strategy = solver_order::implementations::strategies::simple::create_strategy(&strategy_config) .expect("Failed to create strategy"); @@ -424,7 +436,7 @@ mod tests { )); // Create pricing service with mock implementation - let pricing_config = toml::Value::Table(toml::value::Table::new()); + let pricing_config = serde_json::Value::Object(serde_json::Map::new()); let pricing_impl = solver_pricing::implementations::mock::create_mock_pricing(&pricing_config) .expect("Failed to create mock pricing"); @@ -716,117 +728,146 @@ mod tests { /// Creates a test Config for testing `*_from_config` functions. fn create_test_config() -> Config { - let config_toml = r#" - [solver] - id = "test-solver" - monitoring_timeout_seconds = 30 - min_profitability_pct = 1.0 - - [storage] - primary = "memory" - cleanup_interval_seconds = 3600 - [storage.implementations.memory] - - [delivery] - min_confirmations = 1 - [delivery.implementations] - - [account] - primary = "local" - [account.implementations.local] - private_key = "0x1234567890123456789012345678901234567890123456789012345678901234" - - [discovery] - [discovery.implementations] - - [order] - [order.implementations] - [order.strategy] - primary = "simple" - [order.strategy.implementations.simple] - - [settlement] - [settlement.implementations] - - [networks.1] - chain_id = 1 - input_settler_address = "0x1111111111111111111111111111111111111111" - output_settler_address = "0x2222222222222222222222222222222222222222" - [[networks.1.rpc_urls]] - http = "http://localhost:8545" - [[networks.1.tokens]] - symbol = "USDC" - address = "0x3333333333333333333333333333333333333333" - decimals = 6 - [[networks.1.tokens]] - symbol = "WETH" - address = "0x4444444444444444444444444444444444444444" - decimals = 18 - - [networks.137] - chain_id = 137 - input_settler_address = "0x5555555555555555555555555555555555555555" - output_settler_address = "0x6666666666666666666666666666666666666666" - [[networks.137.rpc_urls]] - http = "http://localhost:8546" - [[networks.137.tokens]] - symbol = "USDC" - address = "0x7777777777777777777777777777777777777777" - decimals = 6 - "#; - toml::from_str(config_toml).expect("Failed to parse test config") + serde_json::from_value(serde_json::json!({ + "solver": { + "id": "test-solver", + "monitoring_timeout_seconds": 30, + "min_profitability_pct": 1.0 + }, + "storage": { + "primary": "memory", + "cleanup_interval_seconds": 3600, + "implementations": { + "memory": {} + } + }, + "delivery": { + "min_confirmations": 1, + "implementations": {} + }, + "account": { + "primary": "local", + "implementations": { + "local": { + "private_key": "0x1234567890123456789012345678901234567890123456789012345678901234" + } + } + }, + "discovery": { + "implementations": {} + }, + "order": { + "implementations": {}, + "strategy": { + "primary": "simple", + "implementations": { + "simple": {} + } + } + }, + "settlement": { + "implementations": {} + }, + "networks": { + "1": { + "chain_id": 1, + "input_settler_address": "0x1111111111111111111111111111111111111111", + "output_settler_address": "0x2222222222222222222222222222222222222222", + "rpc_urls": [ + { "http": "http://localhost:8545" } + ], + "tokens": [ + { + "symbol": "USDC", + "address": "0x3333333333333333333333333333333333333333", + "decimals": 6 + }, + { + "symbol": "WETH", + "address": "0x4444444444444444444444444444444444444444", + "decimals": 18 + } + ] + }, + "137": { + "chain_id": 137, + "input_settler_address": "0x5555555555555555555555555555555555555555", + "output_settler_address": "0x6666666666666666666666666666666666666666", + "rpc_urls": [ + { "http": "http://localhost:8546" } + ], + "tokens": [ + { + "symbol": "USDC", + "address": "0x7777777777777777777777777777777777777777", + "decimals": 6 + } + ] + } + } + })) + .expect("Failed to parse test config") } fn create_test_config_with_empty_tokens() -> Config { - let config_toml = r#" - [solver] - id = "test-solver" - monitoring_timeout_seconds = 30 - min_profitability_pct = 1.0 - - [storage] - primary = "memory" - cleanup_interval_seconds = 3600 - [storage.implementations.memory] - - [delivery] - min_confirmations = 1 - [delivery.implementations] - - [account] - primary = "local" - [account.implementations.local] - private_key = "0x1234567890123456789012345678901234567890123456789012345678901234" - - [discovery] - [discovery.implementations] - - [order] - [order.implementations] - [order.strategy] - primary = "simple" - [order.strategy.implementations.simple] - - [settlement] - [settlement.implementations] - - [networks.1] - chain_id = 1 - input_settler_address = "0x1111111111111111111111111111111111111111" - output_settler_address = "0x2222222222222222222222222222222222222222" - tokens = [] - [[networks.1.rpc_urls]] - http = "http://localhost:8545" - - [networks.137] - chain_id = 137 - input_settler_address = "0x5555555555555555555555555555555555555555" - output_settler_address = "0x6666666666666666666666666666666666666666" - tokens = [] - [[networks.137.rpc_urls]] - http = "http://localhost:8546" - "#; - toml::from_str(config_toml).expect("Failed to parse test config with empty tokens") + serde_json::from_value(serde_json::json!({ + "solver": { + "id": "test-solver", + "monitoring_timeout_seconds": 30, + "min_profitability_pct": 1.0 + }, + "storage": { + "primary": "memory", + "cleanup_interval_seconds": 3600, + "implementations": { + "memory": {} + } + }, + "delivery": { + "min_confirmations": 1, + "implementations": {} + }, + "account": { + "primary": "local", + "implementations": { + "local": { + "private_key": "0x1234567890123456789012345678901234567890123456789012345678901234" + } + } + }, + "discovery": { + "implementations": {} + }, + "order": { + "implementations": {}, + "strategy": { + "primary": "simple", + "implementations": { + "simple": {} + } + } + }, + "settlement": { + "implementations": {} + }, + "networks": { + "1": { + "chain_id": 1, + "input_settler_address": "0x1111111111111111111111111111111111111111", + "output_settler_address": "0x2222222222222222222222222222222222222222", + "rpc_urls": [{ "http": "http://localhost:8545" }], + "tokens": [] + }, + "137": { + "chain_id": 137, + "input_settler_address": "0x5555555555555555555555555555555555555555", + "output_settler_address": "0x6666666666666666666666666666666666666666", + "rpc_urls": [{ "http": "http://localhost:8546" }], + "tokens": [] + } + } + })) + .expect("Failed to parse test config with empty tokens") } #[tokio::test] diff --git a/crates/solver-service/src/config_merge.rs b/crates/solver-service/src/config_merge.rs index 800cc1e3..8ebf5d53 100644 --- a/crates/solver-service/src/config_merge.rs +++ b/crates/solver-service/src/config_merge.rs @@ -931,20 +931,23 @@ fn build_storage_config_from_operator(solver_id: &str) -> StorageConfig { // Redis implementation config // Use solver_id as key_prefix to isolate storage per solver instance - let redis_config = toml_table(vec![ - ("redis_url", toml::Value::String(redis_url)), - ("key_prefix", toml::Value::String(solver_id.to_string())), - ("connection_timeout_ms", toml::Value::Integer(5000)), - ("ttl_orders", toml::Value::Integer(0)), - ("ttl_intents", toml::Value::Integer(86400)), - ("ttl_order_by_tx_hash", toml::Value::Integer(86400)), + let redis_config = json_object(vec![ + ("redis_url", serde_json::Value::String(redis_url)), + ( + "key_prefix", + serde_json::Value::String(solver_id.to_string()), + ), + ("connection_timeout_ms", int(5000)), + ("ttl_orders", int(0)), + ("ttl_intents", int(86400)), + ("ttl_order_by_tx_hash", int(86400)), ]); implementations.insert("redis".to_string(), redis_config); // Memory implementation (fallback for testing) implementations.insert( "memory".to_string(), - toml::Value::Table(toml::map::Map::new()), + serde_json::Value::Object(serde_json::Map::new()), ); StorageConfig { @@ -958,14 +961,10 @@ fn build_storage_config_from_operator(solver_id: &str) -> StorageConfig { fn build_delivery_config_from_operator(chain_ids: &[u64]) -> DeliveryConfig { let mut implementations = HashMap::new(); - let network_ids_array = toml::Value::Array( - chain_ids - .iter() - .map(|id| toml::Value::Integer(*id as i64)) - .collect(), - ); + let network_ids_array = + serde_json::Value::Array(chain_ids.iter().map(|id| int(*id as i64)).collect()); - let evm_alloy_config = toml_table(vec![("network_ids", network_ids_array)]); + let evm_alloy_config = json_object(vec![("network_ids", network_ids_array)]); implementations.insert("evm_alloy".to_string(), evm_alloy_config); DeliveryConfig { @@ -991,8 +990,8 @@ fn build_account_config_from_operator( let mut implementations = HashMap::new(); for (name, json_value) in &config.implementations { - let toml_value = json_to_toml(json_value); - implementations.insert(name.clone(), toml_value); + let impl_value = json_value.clone(); + implementations.insert(name.clone(), impl_value); } return AccountConfig { @@ -1010,7 +1009,10 @@ fn build_account_config_from_operator( .map(|k| k.trim().to_string()) .unwrap_or_else(|_| "${SOLVER_PRIVATE_KEY}".to_string()); - let local_config = toml_table(vec![("private_key", toml::Value::String(private_key))]); + let local_config = json_object(vec![( + "private_key", + serde_json::Value::String(private_key), + )]); implementations.insert("local".to_string(), local_config); AccountConfig { @@ -1023,24 +1025,20 @@ fn build_account_config_from_operator( fn build_discovery_config_from_operator(chain_ids: &[u64]) -> DiscoveryConfig { let mut implementations = HashMap::new(); - let network_ids_array = toml::Value::Array( - chain_ids - .iter() - .map(|id| toml::Value::Integer(*id as i64)) - .collect(), - ); + let network_ids_array = + serde_json::Value::Array(chain_ids.iter().map(|id| int(*id as i64)).collect()); // Onchain discovery - polls chain for new orders - let onchain_config = toml_table(vec![ + let onchain_config = json_object(vec![ ("network_ids", network_ids_array.clone()), - ("polling_interval_secs", toml::Value::Integer(5)), + ("polling_interval_secs", int(5)), ]); implementations.insert("onchain_eip7683".to_string(), onchain_config); // Offchain discovery - receives orders via HTTP API from aggregators - let offchain_config = toml_table(vec![ - ("api_host", toml::Value::String("0.0.0.0".to_string())), - ("api_port", toml::Value::Integer(8081)), + let offchain_config = json_object(vec![ + ("api_host", serde_json::Value::String("0.0.0.0".to_string())), + ("api_port", int(8081)), ("network_ids", network_ids_array), ]); implementations.insert("offchain_eip7683".to_string(), offchain_config); @@ -1055,13 +1053,12 @@ fn build_order_config_from_operator() -> OrderConfig { // EIP-7683 order implementation implementations.insert( "eip7683".to_string(), - toml::Value::Table(toml::map::Map::new()), + serde_json::Value::Object(serde_json::Map::new()), ); // Strategy implementations let mut strategy_implementations = HashMap::new(); - let simple_strategy_config = - toml_table(vec![("max_gas_price_gwei", toml::Value::Integer(100))]); + let simple_strategy_config = json_object(vec![("max_gas_price_gwei", int(100))]); strategy_implementations.insert("simple".to_string(), simple_strategy_config); OrderConfig { @@ -1094,7 +1091,7 @@ fn build_settlement_config_from_operator( .to_string(), ) })?; - let hyperlane_config = build_hyperlane_toml_from_operator(hyperlane, chain_ids); + let hyperlane_config = build_hyperlane_json_from_operator(hyperlane, chain_ids); implementations.insert("hyperlane".to_string(), hyperlane_config); }, OperatorSettlementType::Direct => { @@ -1116,181 +1113,178 @@ fn build_settlement_config_from_operator( }) } -/// Builds Hyperlane toml config from OperatorHyperlaneConfig. -fn build_hyperlane_toml_from_operator( +/// Builds Hyperlane JSON config from OperatorHyperlaneConfig. +fn build_hyperlane_json_from_operator( hyperlane: &OperatorHyperlaneConfig, chain_ids: &[u64], -) -> toml::Value { - let mut table = toml::map::Map::new(); +) -> serde_json::Value { + let mut table = serde_json::Map::new(); // Basic settings table.insert( "order".to_string(), - toml::Value::String("eip7683".to_string()), + serde_json::Value::String("eip7683".to_string()), ); table.insert( "network_ids".to_string(), - toml::Value::Array( - chain_ids - .iter() - .map(|id| toml::Value::Integer(*id as i64)) - .collect(), - ), + serde_json::Value::Array(chain_ids.iter().map(|id| int(*id as i64)).collect()), ); table.insert( "default_gas_limit".to_string(), - toml::Value::Integer(hyperlane.default_gas_limit as i64), + int(hyperlane.default_gas_limit as i64), ); table.insert( "message_timeout_seconds".to_string(), - toml::Value::Integer(hyperlane.message_timeout_seconds as i64), + int(hyperlane.message_timeout_seconds as i64), ); table.insert( "finalization_required".to_string(), - toml::Value::Boolean(hyperlane.finalization_required), + serde_json::Value::Bool(hyperlane.finalization_required), ); // Build oracles map - let mut input_oracles = toml::map::Map::new(); - let mut output_oracles = toml::map::Map::new(); + let mut input_oracles = serde_json::Map::new(); + let mut output_oracles = serde_json::Map::new(); for (chain_id, oracles) in &hyperlane.oracles.input { - let oracle_array = toml::Value::Array( + let oracle_array = serde_json::Value::Array( oracles .iter() - .map(|addr| toml::Value::String(format!("0x{}", hex::encode(addr)))) + .map(|addr| serde_json::Value::String(format!("0x{}", hex::encode(addr)))) .collect(), ); input_oracles.insert(chain_id.to_string(), oracle_array); } for (chain_id, oracles) in &hyperlane.oracles.output { - let oracle_array = toml::Value::Array( + let oracle_array = serde_json::Value::Array( oracles .iter() - .map(|addr| toml::Value::String(format!("0x{}", hex::encode(addr)))) + .map(|addr| serde_json::Value::String(format!("0x{}", hex::encode(addr)))) .collect(), ); output_oracles.insert(chain_id.to_string(), oracle_array); } - let mut oracles = toml::map::Map::new(); - oracles.insert("input".to_string(), toml::Value::Table(input_oracles)); - oracles.insert("output".to_string(), toml::Value::Table(output_oracles)); - table.insert("oracles".to_string(), toml::Value::Table(oracles)); + let mut oracles = serde_json::Map::new(); + oracles.insert( + "input".to_string(), + serde_json::Value::Object(input_oracles), + ); + oracles.insert( + "output".to_string(), + serde_json::Value::Object(output_oracles), + ); + table.insert("oracles".to_string(), serde_json::Value::Object(oracles)); // Build routes - let mut routes = toml::map::Map::new(); + let mut routes = serde_json::Map::new(); for (chain_id, destinations) in &hyperlane.routes { - let dest_array = toml::Value::Array( - destinations - .iter() - .map(|c| toml::Value::Integer(*c as i64)) - .collect(), - ); + let dest_array = + serde_json::Value::Array(destinations.iter().map(|c| int(*c as i64)).collect()); routes.insert(chain_id.to_string(), dest_array); } - table.insert("routes".to_string(), toml::Value::Table(routes)); + table.insert("routes".to_string(), serde_json::Value::Object(routes)); // Build mailboxes map - let mut mailboxes = toml::map::Map::new(); + let mut mailboxes = serde_json::Map::new(); for (chain_id, addr) in &hyperlane.mailboxes { mailboxes.insert( chain_id.to_string(), - toml::Value::String(format!("0x{}", hex::encode(addr))), + serde_json::Value::String(format!("0x{}", hex::encode(addr))), ); } - table.insert("mailboxes".to_string(), toml::Value::Table(mailboxes)); + table.insert( + "mailboxes".to_string(), + serde_json::Value::Object(mailboxes), + ); // Build IGP addresses map - let mut igp_addresses = toml::map::Map::new(); + let mut igp_addresses = serde_json::Map::new(); for (chain_id, addr) in &hyperlane.igp_addresses { igp_addresses.insert( chain_id.to_string(), - toml::Value::String(format!("0x{}", hex::encode(addr))), + serde_json::Value::String(format!("0x{}", hex::encode(addr))), ); } table.insert( "igp_addresses".to_string(), - toml::Value::Table(igp_addresses), + serde_json::Value::Object(igp_addresses), ); - toml::Value::Table(table) + serde_json::Value::Object(table) } -/// Builds direct settlement toml config from OperatorDirectConfig. +/// Builds direct settlement JSON config from OperatorDirectConfig. fn build_direct_toml_from_operator( direct: &OperatorDirectConfig, chain_ids: &[u64], -) -> toml::Value { - let mut table = toml::map::Map::new(); +) -> serde_json::Value { + let mut table = serde_json::Map::new(); table.insert( "order".to_string(), - toml::Value::String("eip7683".to_string()), + serde_json::Value::String("eip7683".to_string()), ); table.insert( "network_ids".to_string(), - toml::Value::Array( - chain_ids - .iter() - .map(|id| toml::Value::Integer(*id as i64)) - .collect(), - ), + serde_json::Value::Array(chain_ids.iter().map(|id| int(*id as i64)).collect()), ); table.insert( "dispute_period_seconds".to_string(), - toml::Value::Integer(direct.dispute_period_seconds as i64), + int(direct.dispute_period_seconds as i64), ); table.insert( "oracle_selection_strategy".to_string(), - toml::Value::String(match direct.oracle_selection_strategy { + serde_json::Value::String(match direct.oracle_selection_strategy { OperatorOracleSelectionStrategy::First => "First".to_string(), OperatorOracleSelectionStrategy::RoundRobin => "RoundRobin".to_string(), OperatorOracleSelectionStrategy::Random => "Random".to_string(), }), ); - let mut input_oracles = toml::map::Map::new(); + let mut input_oracles = serde_json::Map::new(); for (chain_id, oracles) in &direct.oracles.input { - let oracle_array = toml::Value::Array( + let oracle_array = serde_json::Value::Array( oracles .iter() - .map(|addr| toml::Value::String(format!("0x{}", hex::encode(addr)))) + .map(|addr| serde_json::Value::String(format!("0x{}", hex::encode(addr)))) .collect(), ); input_oracles.insert(chain_id.to_string(), oracle_array); } - let mut output_oracles = toml::map::Map::new(); + let mut output_oracles = serde_json::Map::new(); for (chain_id, oracles) in &direct.oracles.output { - let oracle_array = toml::Value::Array( + let oracle_array = serde_json::Value::Array( oracles .iter() - .map(|addr| toml::Value::String(format!("0x{}", hex::encode(addr)))) + .map(|addr| serde_json::Value::String(format!("0x{}", hex::encode(addr)))) .collect(), ); output_oracles.insert(chain_id.to_string(), oracle_array); } - let mut oracles = toml::map::Map::new(); - oracles.insert("input".to_string(), toml::Value::Table(input_oracles)); - oracles.insert("output".to_string(), toml::Value::Table(output_oracles)); - table.insert("oracles".to_string(), toml::Value::Table(oracles)); + let mut oracles = serde_json::Map::new(); + oracles.insert( + "input".to_string(), + serde_json::Value::Object(input_oracles), + ); + oracles.insert( + "output".to_string(), + serde_json::Value::Object(output_oracles), + ); + table.insert("oracles".to_string(), serde_json::Value::Object(oracles)); - let mut routes = toml::map::Map::new(); + let mut routes = serde_json::Map::new(); for (chain_id, destinations) in &direct.routes { - let dest_array = toml::Value::Array( - destinations - .iter() - .map(|c| toml::Value::Integer(*c as i64)) - .collect(), - ); + let dest_array = + serde_json::Value::Array(destinations.iter().map(|c| int(*c as i64)).collect()); routes.insert(chain_id.to_string(), dest_array); } - table.insert("routes".to_string(), toml::Value::Table(routes)); + table.insert("routes".to_string(), serde_json::Value::Object(routes)); - toml::Value::Table(table) + serde_json::Value::Object(table) } /// Builds PricingConfig from OperatorPricingConfig. @@ -1298,19 +1292,19 @@ fn build_pricing_config_from_operator(pricing: &OperatorPricingConfig) -> Pricin let mut implementations = HashMap::new(); // CoinGecko implementation - let coingecko_config = toml_table(vec![ + let coingecko_config = json_object(vec![ ( "cache_duration_seconds", - toml::Value::Integer(pricing.cache_duration_seconds as i64), + int(pricing.cache_duration_seconds as i64), ), - ("rate_limit_delay_ms", toml::Value::Integer(1200)), + ("rate_limit_delay_ms", int(1200)), ]); implementations.insert("coingecko".to_string(), coingecko_config); // DefiLlama implementation - let defillama_config = toml_table(vec![( + let defillama_config = json_object(vec![( "cache_duration_seconds", - toml::Value::Integer(pricing.cache_duration_seconds as i64), + int(pricing.cache_duration_seconds as i64), )]); implementations.insert("defillama".to_string(), defillama_config); @@ -1411,39 +1405,17 @@ fn build_api_config_from_operator( }) } -/// Helper to create a toml::Value::Table from key-value pairs -fn toml_table(pairs: Vec<(&str, toml::Value)>) -> toml::Value { - let mut table = toml::map::Map::new(); +/// Helper to create a serde_json::Value::Object from key-value pairs +fn json_object(pairs: Vec<(&str, serde_json::Value)>) -> serde_json::Value { + let mut table = serde_json::Map::new(); for (key, value) in pairs { table.insert(key.to_string(), value); } - toml::Value::Table(table) + serde_json::Value::Object(table) } -/// Converts a serde_json::Value to a toml::Value. -fn json_to_toml(json: &serde_json::Value) -> toml::Value { - match json { - serde_json::Value::Null => toml::Value::String("".to_string()), - serde_json::Value::Bool(b) => toml::Value::Boolean(*b), - serde_json::Value::Number(n) => { - if let Some(i) = n.as_i64() { - toml::Value::Integer(i) - } else if let Some(f) = n.as_f64() { - toml::Value::Float(f) - } else { - toml::Value::String(n.to_string()) - } - }, - serde_json::Value::String(s) => toml::Value::String(s.clone()), - serde_json::Value::Array(arr) => toml::Value::Array(arr.iter().map(json_to_toml).collect()), - serde_json::Value::Object(obj) => { - let mut table = toml::map::Map::new(); - for (k, v) in obj { - table.insert(k.clone(), json_to_toml(v)); - } - toml::Value::Table(table) - }, - } +fn int(value: i64) -> serde_json::Value { + serde_json::Value::Number(value.into()) } /// Converts an existing Config to OperatorConfig. @@ -1454,7 +1426,7 @@ fn json_to_toml(json: &serde_json::Value) -> toml::Value { /// OperatorConfig for admin API persistence. /// /// Note: Some information (like Hyperlane addresses) may be incomplete -/// as Config stores them in toml format. The function does best-effort +/// as Config stores them as JSON values. The function does best-effort /// extraction. pub fn config_to_operator_config(config: &Config) -> Result { use alloy_primitives::Address; @@ -1671,12 +1643,12 @@ fn extract_account_config(account: &AccountConfig) -> Option Option serde_json::Value { - match toml { - toml::Value::String(s) => serde_json::Value::String(s.clone()), - toml::Value::Integer(i) => serde_json::Value::Number((*i).into()), - toml::Value::Float(f) => serde_json::Number::from_f64(*f) - .map(serde_json::Value::Number) - .unwrap_or(serde_json::Value::Null), - toml::Value::Boolean(b) => serde_json::Value::Bool(*b), - toml::Value::Datetime(dt) => serde_json::Value::String(dt.to_string()), - toml::Value::Array(arr) => serde_json::Value::Array(arr.iter().map(toml_to_json).collect()), - toml::Value::Table(table) => { - let map: serde_json::Map = table - .iter() - .map(|(k, v)| (k.clone(), toml_to_json(v))) - .collect(); - serde_json::Value::Object(map) - }, - } +/// Converts a serde_json::Value to a serde_json::Value. +fn json_clone(value: &serde_json::Value) -> serde_json::Value { + value.clone() } -/// Extracts Hyperlane config from settlement toml config. +/// Extracts Hyperlane config from settlement JSON config. fn extract_hyperlane_config( settlement: &SettlementConfig, chain_ids: &[u64], ) -> OperatorHyperlaneConfig { use alloy_primitives::Address; - let hyperlane_toml = settlement.implementations.get("hyperlane"); + let hyperlane_json = settlement.implementations.get("hyperlane"); // Helper to parse address from hex string let parse_addr = |s: &str| -> Option
{ @@ -1726,28 +1682,28 @@ fn extract_hyperlane_config( }) }; - let default_gas_limit = hyperlane_toml + let default_gas_limit = hyperlane_json .and_then(|h| h.get("default_gas_limit")) - .and_then(|v| v.as_integer()) + .and_then(|v| v.as_i64()) .unwrap_or(300_000) as u64; - let message_timeout_seconds = hyperlane_toml + let message_timeout_seconds = hyperlane_json .and_then(|h| h.get("message_timeout_seconds")) - .and_then(|v| v.as_integer()) + .and_then(|v| v.as_i64()) .unwrap_or(600) as u64; - let finalization_required = hyperlane_toml + let finalization_required = hyperlane_json .and_then(|h| h.get("finalization_required")) .and_then(|v| v.as_bool()) .unwrap_or(true); // Extract mailboxes let mut mailboxes = HashMap::new(); - if let Some(toml_mailboxes) = hyperlane_toml + if let Some(json_mailboxes) = hyperlane_json .and_then(|h| h.get("mailboxes")) - .and_then(|v| v.as_table()) + .and_then(|v| v.as_object()) { - for (chain_id_str, addr_val) in toml_mailboxes { + for (chain_id_str, addr_val) in json_mailboxes { if let (Ok(chain_id), Some(addr_str)) = (chain_id_str.parse::(), addr_val.as_str()) { if let Some(addr) = parse_addr(addr_str) { @@ -1759,11 +1715,11 @@ fn extract_hyperlane_config( // Extract IGP addresses let mut igp_addresses = HashMap::new(); - if let Some(toml_igp) = hyperlane_toml + if let Some(json_igp) = hyperlane_json .and_then(|h| h.get("igp_addresses")) - .and_then(|v| v.as_table()) + .and_then(|v| v.as_object()) { - for (chain_id_str, addr_val) in toml_igp { + for (chain_id_str, addr_val) in json_igp { if let (Ok(chain_id), Some(addr_str)) = (chain_id_str.parse::(), addr_val.as_str()) { if let Some(addr) = parse_addr(addr_str) { @@ -1776,11 +1732,11 @@ fn extract_hyperlane_config( // Extract oracles let mut input_oracles = HashMap::new(); let mut output_oracles = HashMap::new(); - if let Some(toml_oracles) = hyperlane_toml + if let Some(json_oracles) = hyperlane_json .and_then(|h| h.get("oracles")) - .and_then(|v| v.as_table()) + .and_then(|v| v.as_object()) { - if let Some(input_table) = toml_oracles.get("input").and_then(|v| v.as_table()) { + if let Some(input_table) = json_oracles.get("input").and_then(|v| v.as_object()) { for (chain_id_str, addrs_val) in input_table { if let (Ok(chain_id), Some(addrs_array)) = (chain_id_str.parse::(), addrs_val.as_array()) @@ -1795,7 +1751,7 @@ fn extract_hyperlane_config( } } } - if let Some(output_table) = toml_oracles.get("output").and_then(|v| v.as_table()) { + if let Some(output_table) = json_oracles.get("output").and_then(|v| v.as_object()) { for (chain_id_str, addrs_val) in output_table { if let (Ok(chain_id), Some(addrs_array)) = (chain_id_str.parse::(), addrs_val.as_array()) @@ -1814,17 +1770,17 @@ fn extract_hyperlane_config( // Extract routes let mut routes = HashMap::new(); - if let Some(toml_routes) = hyperlane_toml + if let Some(json_routes) = hyperlane_json .and_then(|h| h.get("routes")) - .and_then(|v| v.as_table()) + .and_then(|v| v.as_object()) { - for (chain_id_str, dests_val) in toml_routes { + for (chain_id_str, dests_val) in json_routes { if let (Ok(chain_id), Some(dests_array)) = (chain_id_str.parse::(), dests_val.as_array()) { let dests: Vec = dests_array .iter() - .filter_map(|v| v.as_integer().map(|i| i as u64)) + .filter_map(|v| v.as_i64().map(|i| i as u64)) .collect(); routes.insert(chain_id, dests); } @@ -1874,7 +1830,7 @@ fn extract_direct_config(settlement: &SettlementConfig, chain_ids: &[u64]) -> Op let dispute_period_seconds = direct_toml .and_then(|d| d.get("dispute_period_seconds")) - .and_then(|v| v.as_integer()) + .and_then(|v| v.as_i64()) .unwrap_or(300) as u64; let oracle_selection_strategy = direct_toml @@ -1891,9 +1847,9 @@ fn extract_direct_config(settlement: &SettlementConfig, chain_ids: &[u64]) -> Op let mut output_oracles = HashMap::new(); if let Some(toml_oracles) = direct_toml .and_then(|d| d.get("oracles")) - .and_then(|v| v.as_table()) + .and_then(|v| v.as_object()) { - if let Some(input_table) = toml_oracles.get("input").and_then(|v| v.as_table()) { + if let Some(input_table) = toml_oracles.get("input").and_then(|v| v.as_object()) { for (chain_id_str, addrs_val) in input_table { if let (Ok(chain_id), Some(addrs_array)) = (chain_id_str.parse::(), addrs_val.as_array()) @@ -1909,7 +1865,7 @@ fn extract_direct_config(settlement: &SettlementConfig, chain_ids: &[u64]) -> Op } } - if let Some(output_table) = toml_oracles.get("output").and_then(|v| v.as_table()) { + if let Some(output_table) = toml_oracles.get("output").and_then(|v| v.as_object()) { for (chain_id_str, addrs_val) in output_table { if let (Ok(chain_id), Some(addrs_array)) = (chain_id_str.parse::(), addrs_val.as_array()) @@ -1929,7 +1885,7 @@ fn extract_direct_config(settlement: &SettlementConfig, chain_ids: &[u64]) -> Op let mut routes = HashMap::new(); if let Some(toml_routes) = direct_toml .and_then(|d| d.get("routes")) - .and_then(|v| v.as_table()) + .and_then(|v| v.as_object()) { for (chain_id_str, dests_val) in toml_routes { if let (Ok(chain_id), Some(dests_array)) = @@ -1937,7 +1893,7 @@ fn extract_direct_config(settlement: &SettlementConfig, chain_ids: &[u64]) -> Op { let dests: Vec = dests_array .iter() - .filter_map(|v| v.as_integer().map(|i| i as u64)) + .filter_map(|v| v.as_i64().map(|i| i as u64)) .collect(); routes.insert(chain_id, dests); } @@ -2391,15 +2347,15 @@ mod tests { let config = merge_config(overrides, &TESTNET_SEED).unwrap(); let hyperlane = config.settlement.implementations.get("hyperlane").unwrap(); - let routes = hyperlane.get("routes").unwrap().as_table().unwrap(); + let routes = hyperlane.get("routes").unwrap().as_object().unwrap(); // Check Optimism Sepolia can send to Base Sepolia let opt_routes = routes.get("11155420").unwrap().as_array().unwrap(); - assert!(opt_routes.contains(&toml::Value::Integer(84532))); + assert!(opt_routes.contains(&int(84532))); // Check Base Sepolia can send to Optimism Sepolia let base_routes = routes.get("84532").unwrap().as_array().unwrap(); - assert!(base_routes.contains(&toml::Value::Integer(11155420))); + assert!(base_routes.contains(&int(11155420))); } #[test] @@ -3983,17 +3939,17 @@ mod tests { // ===== Tests for helper functions ===== #[test] - fn test_toml_table_helper() { - let table = toml_table(vec![ - ("key1", toml::Value::String("value1".to_string())), - ("key2", toml::Value::Integer(42)), - ("key3", toml::Value::Boolean(true)), + fn test_json_object_helper() { + let table = json_object(vec![ + ("key1", serde_json::Value::String("value1".to_string())), + ("key2", int(42)), + ("key3", serde_json::Value::Bool(true)), ]); - assert!(table.is_table()); - let t = table.as_table().unwrap(); + assert!(table.is_object()); + let t = table.as_object().unwrap(); assert_eq!(t.get("key1").unwrap().as_str().unwrap(), "value1"); - assert_eq!(t.get("key2").unwrap().as_integer().unwrap(), 42); + assert_eq!(t.get("key2").unwrap().as_i64().unwrap(), 42); assert!(t.get("key3").unwrap().as_bool().unwrap()); } diff --git a/crates/solver-service/src/factory_registry.rs b/crates/solver-service/src/factory_registry.rs index 7565444e..55da99a7 100644 --- a/crates/solver-service/src/factory_registry.rs +++ b/crates/solver-service/src/factory_registry.rs @@ -22,30 +22,31 @@ use tokio::sync::RwLock; // Type aliases for factory functions pub type AccountFactory = for<'a> fn( - &'a toml::Value, + &'a serde_json::Value, ) -> Pin< Box, AccountError>> + Send + 'a>, >; pub type DeliveryFactory = fn( - &toml::Value, + &serde_json::Value, &NetworksConfig, &solver_account::AccountSigner, &std::collections::HashMap, ) -> Result, DeliveryError>; pub type DiscoveryFactory = - fn(&toml::Value, &NetworksConfig) -> Result, DiscoveryError>; + fn(&serde_json::Value, &NetworksConfig) -> Result, DiscoveryError>; pub type OrderFactory = fn( - &toml::Value, + &serde_json::Value, &NetworksConfig, &solver_types::oracle::OracleRoutes, ) -> Result, OrderError>; -pub type PricingFactory = fn(&toml::Value) -> Result, PricingError>; +pub type PricingFactory = fn(&serde_json::Value) -> Result, PricingError>; pub type SettlementFactory = fn( - &toml::Value, + &serde_json::Value, &NetworksConfig, std::sync::Arc, ) -> Result, SettlementError>; -pub type StrategyFactory = fn(&toml::Value) -> Result, StrategyError>; +pub type StrategyFactory = + fn(&serde_json::Value) -> Result, StrategyError>; /// Global registry for all implementation factories #[derive(Default)] @@ -292,53 +293,68 @@ mod tests { #[tokio::test] async fn build_solver_from_config_errors_on_unknown_delivery_impl() { - let config_toml = r#" - [solver] - id = "test-solver" - monitoring_timeout_seconds = 30 - min_profitability_pct = 1.0 - - [storage] - primary = "memory" - cleanup_interval_seconds = 60 - [storage.implementations.memory] - - [delivery] - min_confirmations = 1 - primary = "unknown" - [delivery.implementations] - unknown = {} - - [account] - primary = "local" - [account.implementations.local] - private_key = "0x1234567890123456789012345678901234567890123456789012345678901234" - - [discovery] - [discovery.implementations] - - [order] - [order.implementations] - [order.strategy] - primary = "simple" - [order.strategy.implementations.simple] - - [settlement] - [settlement.implementations] - - [networks.1] - chain_id = 1 - input_settler_address = "0x0000000000000000000000000000000000000001" - output_settler_address = "0x0000000000000000000000000000000000000002" - [[networks.1.rpc_urls]] - http = "http://localhost:8545" - [[networks.1.tokens]] - symbol = "TEST" - address = "0x0000000000000000000000000000000000000003" - decimals = 18 - "#; - - let config: Config = toml::from_str(config_toml).expect("config parses"); + let config: Config = serde_json::from_value(serde_json::json!({ + "solver": { + "id": "test-solver", + "monitoring_timeout_seconds": 30, + "min_profitability_pct": 1.0 + }, + "storage": { + "primary": "memory", + "cleanup_interval_seconds": 60, + "implementations": { + "memory": {} + } + }, + "delivery": { + "min_confirmations": 1, + "primary": "unknown", + "implementations": { + "unknown": {} + } + }, + "account": { + "primary": "local", + "implementations": { + "local": { + "private_key": "0x1234567890123456789012345678901234567890123456789012345678901234" + } + } + }, + "discovery": { + "implementations": {} + }, + "order": { + "implementations": {}, + "strategy": { + "primary": "simple", + "implementations": { + "simple": {} + } + } + }, + "settlement": { + "implementations": {} + }, + "networks": { + "1": { + "chain_id": 1, + "input_settler_address": "0x0000000000000000000000000000000000000001", + "output_settler_address": "0x0000000000000000000000000000000000000002", + "rpc_urls": [ + { "http": "http://localhost:8545" } + ], + "tokens": [ + { + "symbol": "TEST", + "address": "0x0000000000000000000000000000000000000003", + "decimals": 18 + } + ] + } + } + })) + .expect("config parses"); let dynamic_config = Arc::new(RwLock::new(config)); let message = match build_solver_from_config(dynamic_config).await { Ok(_) => panic!("expected failure"), diff --git a/crates/solver-service/src/server.rs b/crates/solver-service/src/server.rs index 4b9c94c1..c42e0901 100644 --- a/crates/solver-service/src/server.rs +++ b/crates/solver-service/src/server.rs @@ -932,58 +932,71 @@ mod tests { use wiremock::{Mock, MockServer, ResponseTemplate}; async fn build_test_solver_engine() -> Arc { - let config_toml = r#" - [solver] - id = "test-solver" - monitoring_timeout_seconds = 30 - min_profitability_pct = 1.0 - - [storage] - primary = "memory" - cleanup_interval_seconds = 60 - [storage.implementations.memory] - - [delivery] - min_confirmations = 1 - [delivery.implementations] - - [account] - primary = "local" - [account.implementations.local] - private_key = "0x1234567890123456789012345678901234567890123456789012345678901234" - - [discovery] - [discovery.implementations] - - [order] - [order.implementations] - [order.strategy] - primary = "simple" - [order.strategy.implementations.simple] - - [settlement] - [settlement.implementations] - - [networks.1] - chain_id = 1 - input_settler_address = "0x0000000000000000000000000000000000000011" - output_settler_address = "0x0000000000000000000000000000000000000022" - [[networks.1.rpc_urls]] - http = "http://localhost:8545" - [[networks.1.tokens]] - symbol = "TEST" - address = "0x0000000000000000000000000000000000000033" - decimals = 18 - "#; - - let config: Config = toml::from_str(config_toml).expect("failed to parse test config"); + let config: Config = serde_json::from_value(json!({ + "solver": { + "id": "test-solver", + "monitoring_timeout_seconds": 30, + "min_profitability_pct": 1.0 + }, + "storage": { + "primary": "memory", + "cleanup_interval_seconds": 60, + "implementations": { + "memory": {} + } + }, + "delivery": { + "min_confirmations": 1, + "implementations": {} + }, + "account": { + "primary": "local", + "implementations": { + "local": { + "private_key": "0x1234567890123456789012345678901234567890123456789012345678901234" + } + } + }, + "discovery": { + "implementations": {} + }, + "order": { + "implementations": {}, + "strategy": { + "primary": "simple", + "implementations": { + "simple": {} + } + } + }, + "settlement": { + "implementations": {} + }, + "networks": { + "1": { + "chain_id": 1, + "input_settler_address": "0x0000000000000000000000000000000000000011", + "output_settler_address": "0x0000000000000000000000000000000000000022", + "rpc_urls": [ + { "http": "http://localhost:8545" } + ], + "tokens": [ + { + "symbol": "TEST", + "address": "0x0000000000000000000000000000000000000033", + "decimals": 18 + } + ] + } + } + })) + .expect("failed to parse test config"); let storage = Arc::new(StorageService::new(Box::new(MemoryStorage::new()))); - let account_config: toml::Value = toml::from_str( - r#"private_key = "0x1234567890123456789012345678901234567890123456789012345678901234""#, - ) - .expect("failed to parse account config"); + let account_config: serde_json::Value = json!({ + "private_key": "0x1234567890123456789012345678901234567890123456789012345678901234" + }); let account_impl = solver_account::implementations::local::create_account(&account_config) .await .expect("failed to create account impl"); @@ -994,7 +1007,7 @@ mod tests { let delivery = Arc::new(DeliveryService::new(HashMap::new(), 1, 10)); let discovery = Arc::new(DiscoveryService::new(HashMap::new())); - let strategy_config = toml::Value::Table(toml::value::Table::new()); + let strategy_config = serde_json::Value::Object(serde_json::Map::new()); let strategy = solver_order::implementations::strategies::simple::create_strategy(&strategy_config) .expect("failed to create order strategy"); @@ -1002,7 +1015,7 @@ mod tests { let settlement = Arc::new(SettlementService::new(HashMap::new(), 10)); - let pricing_impl = create_mock_pricing(&toml::Value::Table(toml::value::Table::new())) + let pricing_impl = create_mock_pricing(&serde_json::Value::Object(serde_json::Map::new())) .expect("failed to create mock pricing"); let pricing = Arc::new(PricingService::new(pricing_impl, Vec::new())); diff --git a/crates/solver-service/src/validators/order.rs b/crates/solver-service/src/validators/order.rs index 53b4b3d5..5916b541 100644 --- a/crates/solver-service/src/validators/order.rs +++ b/crates/solver-service/src/validators/order.rs @@ -359,7 +359,7 @@ mod tests { ))); let delivery = Arc::new(DeliveryService::new(delivery_impls, 1, 3)); let discovery = Arc::new(DiscoveryService::new(HashMap::new())); - let strategy = create_strategy(&toml::Value::Table(toml::map::Map::new())).unwrap(); + let strategy = create_strategy(&serde_json::Value::Object(serde_json::Map::new())).unwrap(); let order = Arc::new(OrderService::new(HashMap::new(), strategy)); let settlement = Arc::new(SettlementService::new(HashMap::new(), 3)); let event_bus = EventBus::new(32); @@ -369,7 +369,7 @@ mod tests { account.clone(), )); let pricing_impl = - mock::create_mock_pricing(&toml::Value::Table(toml::map::Map::new())).unwrap(); + mock::create_mock_pricing(&serde_json::Value::Object(serde_json::Map::new())).unwrap(); let pricing = Arc::new(PricingService::new(pricing_impl, Vec::new())); let solver_address = solver_types::parse_address(TEST_SOLVER).unwrap(); diff --git a/crates/solver-settlement/Cargo.toml b/crates/solver-settlement/Cargo.toml index fc4b353e..781b335c 100644 --- a/crates/solver-settlement/Cargo.toml +++ b/crates/solver-settlement/Cargo.toml @@ -21,7 +21,6 @@ solver-storage = { path = "../solver-storage" } solver-types = { path = "../solver-types" } thiserror = "2.0" tokio = { version = "1.0", features = ["rt-multi-thread", "sync"] } -toml = { workspace = true } tracing = "0.1" [features] diff --git a/crates/solver-settlement/src/implementations/direct.rs b/crates/solver-settlement/src/implementations/direct.rs index c1c799b7..2f3a0923 100644 --- a/crates/solver-settlement/src/implementations/direct.rs +++ b/crates/solver-settlement/src/implementations/direct.rs @@ -74,14 +74,16 @@ pub struct DirectSettlementSchema; impl DirectSettlementSchema { /// Static validation method for use before instance creation - pub fn validate_config(config: &toml::Value) -> Result<(), solver_types::ValidationError> { + pub fn validate_config( + config: &serde_json::Value, + ) -> Result<(), solver_types::ValidationError> { let instance = Self; instance.validate(config) } } impl ConfigSchema for DirectSettlementSchema { - fn validate(&self, config: &toml::Value) -> Result<(), solver_types::ValidationError> { + fn validate(&self, config: &serde_json::Value) -> Result<(), solver_types::ValidationError> { let schema = Schema::new( // Required fields vec![ @@ -350,7 +352,7 @@ impl SettlementInterface for DirectSettlement { /// Optional configuration parameters: /// - `oracle_selection_strategy`: Strategy for oracle selection (default: round-robin) pub fn create_settlement( - config: &toml::Value, + config: &serde_json::Value, networks: &NetworksConfig, _storage: std::sync::Arc, ) -> Result, SettlementError> { @@ -363,7 +365,7 @@ pub fn create_settlement( let dispute_period_seconds = config .get("dispute_period_seconds") - .and_then(|v| v.as_integer()) + .and_then(|v| v.as_i64()) .unwrap_or(300) as u64; // 5 minutes default // Create settlement service synchronously @@ -471,30 +473,30 @@ mod tests { #[test] fn test_config_schema_validation_valid() { let schema = DirectSettlementSchema; - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "dispute_period_seconds".to_string(), - toml::Value::Integer(300), + serde_json::Value::from(300), ); table.insert( "oracles".to_string(), - toml::Value::Table({ - let mut oracles = toml::map::Map::new(); + serde_json::Value::Object({ + let mut oracles = serde_json::Map::new(); oracles.insert( "input".to_string(), - toml::Value::Table(toml::map::Map::new()), + serde_json::Value::Object(serde_json::Map::new()), ); oracles.insert( "output".to_string(), - toml::Value::Table(toml::map::Map::new()), + serde_json::Value::Object(serde_json::Map::new()), ); oracles }), ); table.insert( "routes".to_string(), - toml::Value::Table(toml::map::Map::new()), + serde_json::Value::Object(serde_json::Map::new()), ); table }); @@ -506,27 +508,27 @@ mod tests { #[test] fn test_config_schema_validation_missing_required_field() { let schema = DirectSettlementSchema; - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); // Missing dispute_period_seconds table.insert( "oracles".to_string(), - toml::Value::Table({ - let mut oracles = toml::map::Map::new(); + serde_json::Value::Object({ + let mut oracles = serde_json::Map::new(); oracles.insert( "input".to_string(), - toml::Value::Table(toml::map::Map::new()), + serde_json::Value::Object(serde_json::Map::new()), ); oracles.insert( "output".to_string(), - toml::Value::Table(toml::map::Map::new()), + serde_json::Value::Object(serde_json::Map::new()), ); oracles }), ); table.insert( "routes".to_string(), - toml::Value::Table(toml::map::Map::new()), + serde_json::Value::Object(serde_json::Map::new()), ); table }); @@ -538,30 +540,30 @@ mod tests { #[test] fn test_config_schema_validation_invalid_dispute_period() { let schema = DirectSettlementSchema; - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "dispute_period_seconds".to_string(), - toml::Value::Integer(100000), // Too large + serde_json::Value::from(100000), // Too large ); table.insert( "oracles".to_string(), - toml::Value::Table({ - let mut oracles = toml::map::Map::new(); + serde_json::Value::Object({ + let mut oracles = serde_json::Map::new(); oracles.insert( "input".to_string(), - toml::Value::Table(toml::map::Map::new()), + serde_json::Value::Object(serde_json::Map::new()), ); oracles.insert( "output".to_string(), - toml::Value::Table(toml::map::Map::new()), + serde_json::Value::Object(serde_json::Map::new()), ); oracles }), ); table.insert( "routes".to_string(), - toml::Value::Table(toml::map::Map::new()), + serde_json::Value::Object(serde_json::Map::new()), ); table }); @@ -572,30 +574,30 @@ mod tests { #[test] fn test_static_config_validation() { - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "dispute_period_seconds".to_string(), - toml::Value::Integer(300), + serde_json::Value::from(300), ); table.insert( "oracles".to_string(), - toml::Value::Table({ - let mut oracles = toml::map::Map::new(); + serde_json::Value::Object({ + let mut oracles = serde_json::Map::new(); oracles.insert( "input".to_string(), - toml::Value::Table(toml::map::Map::new()), + serde_json::Value::Object(serde_json::Map::new()), ); oracles.insert( "output".to_string(), - toml::Value::Table(toml::map::Map::new()), + serde_json::Value::Object(serde_json::Map::new()), ); oracles }), ); table.insert( "routes".to_string(), - toml::Value::Table(toml::map::Map::new()), + serde_json::Value::Object(serde_json::Map::new()), ); table }); @@ -606,23 +608,23 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn test_create_settlement_success() { - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "dispute_period_seconds".to_string(), - toml::Value::Integer(300), + serde_json::Value::from(300), ); table.insert( "oracles".to_string(), - toml::Value::Table({ - let mut oracles = toml::map::Map::new(); + serde_json::Value::Object({ + let mut oracles = serde_json::Map::new(); oracles.insert( "input".to_string(), - toml::Value::Table({ - let mut input = toml::map::Map::new(); + serde_json::Value::Object({ + let mut input = serde_json::Map::new(); input.insert( "1".to_string(), - toml::Value::Array(vec![toml::Value::String( + serde_json::Value::Array(vec![serde_json::Value::String( "0x1111111111111111111111111111111111111111".to_string(), )]), ); @@ -631,11 +633,11 @@ mod tests { ); oracles.insert( "output".to_string(), - toml::Value::Table({ - let mut output = toml::map::Map::new(); + serde_json::Value::Object({ + let mut output = serde_json::Map::new(); output.insert( "2".to_string(), - toml::Value::Array(vec![toml::Value::String( + serde_json::Value::Array(vec![serde_json::Value::String( "0x2222222222222222222222222222222222222222".to_string(), )]), ); @@ -647,12 +649,12 @@ mod tests { ); table.insert( "routes".to_string(), - toml::Value::Table({ - let mut routes = toml::map::Map::new(); + serde_json::Value::Object({ + let mut routes = serde_json::Map::new(); // Fix: Use correct routes format - chain_id -> [destination_chain_ids] routes.insert( "1".to_string(), - toml::Value::Array(vec![toml::Value::Integer(2)]), + serde_json::Value::Array(vec![serde_json::Value::from(2)]), ); routes }), @@ -668,12 +670,12 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn test_create_settlement_invalid_config() { - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); // Missing required fields table.insert( "dispute_period_seconds".to_string(), - toml::Value::Integer(300), + serde_json::Value::from(300), ); table }); @@ -717,30 +719,30 @@ mod tests { let schema = settlement.config_schema(); // Test valid config - let valid_config = toml::Value::Table({ - let mut table = toml::map::Map::new(); + let valid_config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "dispute_period_seconds".to_string(), - toml::Value::Integer(300), + serde_json::Value::from(300), ); table.insert( "oracles".to_string(), - toml::Value::Table({ - let mut oracles = toml::map::Map::new(); + serde_json::Value::Object({ + let mut oracles = serde_json::Map::new(); oracles.insert( "input".to_string(), - toml::Value::Table(toml::map::Map::new()), + serde_json::Value::Object(serde_json::Map::new()), ); oracles.insert( "output".to_string(), - toml::Value::Table(toml::map::Map::new()), + serde_json::Value::Object(serde_json::Map::new()), ); oracles }), ); table.insert( "routes".to_string(), - toml::Value::Table(toml::map::Map::new()), + serde_json::Value::Object(serde_json::Map::new()), ); table }); @@ -760,23 +762,23 @@ mod tests { let factory = Registry::factory(); // Test that factory function exists and has correct type - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "dispute_period_seconds".to_string(), - toml::Value::Integer(300), + serde_json::Value::from(300), ); table.insert( "oracles".to_string(), - toml::Value::Table({ - let mut oracles = toml::map::Map::new(); + serde_json::Value::Object({ + let mut oracles = serde_json::Map::new(); oracles.insert( "input".to_string(), - toml::Value::Table({ - let mut input = toml::map::Map::new(); + serde_json::Value::Object({ + let mut input = serde_json::Map::new(); input.insert( "1".to_string(), - toml::Value::Array(vec![toml::Value::String( + serde_json::Value::Array(vec![serde_json::Value::String( "0x1111111111111111111111111111111111111111".to_string(), )]), ); @@ -785,11 +787,11 @@ mod tests { ); oracles.insert( "output".to_string(), - toml::Value::Table({ - let mut output = toml::map::Map::new(); + serde_json::Value::Object({ + let mut output = serde_json::Map::new(); output.insert( "2".to_string(), - toml::Value::Array(vec![toml::Value::String( + serde_json::Value::Array(vec![serde_json::Value::String( "0x2222222222222222222222222222222222222222".to_string(), )]), ); @@ -801,12 +803,12 @@ mod tests { ); table.insert( "routes".to_string(), - toml::Value::Table({ - let mut routes = toml::map::Map::new(); + serde_json::Value::Object({ + let mut routes = serde_json::Map::new(); // Fix: routes expects chain_id -> [destination_chain_ids] routes.insert( "1".to_string(), - toml::Value::Array(vec![toml::Value::Integer(2)]), + serde_json::Value::Array(vec![serde_json::Value::from(2)]), ); routes }), @@ -838,34 +840,34 @@ mod tests { #[test] fn test_config_with_optional_fields() { let schema = DirectSettlementSchema; - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "dispute_period_seconds".to_string(), - toml::Value::Integer(300), + serde_json::Value::from(300), ); table.insert( "oracles".to_string(), - toml::Value::Table({ - let mut oracles = toml::map::Map::new(); + serde_json::Value::Object({ + let mut oracles = serde_json::Map::new(); oracles.insert( "input".to_string(), - toml::Value::Table(toml::map::Map::new()), + serde_json::Value::Object(serde_json::Map::new()), ); oracles.insert( "output".to_string(), - toml::Value::Table(toml::map::Map::new()), + serde_json::Value::Object(serde_json::Map::new()), ); oracles }), ); table.insert( "routes".to_string(), - toml::Value::Table(toml::map::Map::new()), + serde_json::Value::Object(serde_json::Map::new()), ); table.insert( "oracle_selection_strategy".to_string(), - toml::Value::String("round_robin".to_string()), + serde_json::Value::String("round_robin".to_string()), ); table }); diff --git a/crates/solver-settlement/src/implementations/hyperlane.rs b/crates/solver-settlement/src/implementations/hyperlane.rs index 15a380c7..d654e5e7 100644 --- a/crates/solver-settlement/src/implementations/hyperlane.rs +++ b/crates/solver-settlement/src/implementations/hyperlane.rs @@ -892,14 +892,16 @@ pub struct HyperlaneSettlementSchema; impl HyperlaneSettlementSchema { /// Static validation method for use before instance creation - pub fn validate_config(config: &toml::Value) -> Result<(), solver_types::ValidationError> { + pub fn validate_config( + config: &serde_json::Value, + ) -> Result<(), solver_types::ValidationError> { let instance = Self; instance.validate(config) } } impl ConfigSchema for HyperlaneSettlementSchema { - fn validate(&self, config: &toml::Value) -> Result<(), solver_types::ValidationError> { + fn validate(&self, config: &serde_json::Value) -> Result<(), solver_types::ValidationError> { let schema = Schema::new( // Required fields vec![ @@ -1318,11 +1320,11 @@ impl SettlementInterface for HyperlaneSettlement { /// Helper function to parse address tables from config fn parse_address_table( - table: &toml::Value, + table: &serde_json::Value, ) -> Result, SettlementError> { let mut result = HashMap::new(); - if let Some(table) = table.as_table() { + if let Some(table) = table.as_object() { for (chain_id_str, address_value) in table { let chain_id = chain_id_str.parse::().map_err(|e| { SettlementError::ValidationFailed(format!("Invalid chain ID '{chain_id_str}': {e}")) @@ -1349,7 +1351,7 @@ fn parse_address_table( /// Factory function to create a Hyperlane settlement provider from configuration pub fn create_settlement( - config: &toml::Value, + config: &serde_json::Value, networks: &NetworksConfig, storage: Arc, ) -> Result, SettlementError> { @@ -1375,7 +1377,7 @@ pub fn create_settlement( let default_gas_limit = config .get("default_gas_limit") - .and_then(|v| v.as_integer()) + .and_then(|v| v.as_i64()) .unwrap_or(500000) as u64; // Create settlement service synchronously diff --git a/crates/solver-settlement/src/lib.rs b/crates/solver-settlement/src/lib.rs index 41f670aa..675a389c 100644 --- a/crates/solver-settlement/src/lib.rs +++ b/crates/solver-settlement/src/lib.rs @@ -242,7 +242,7 @@ pub trait SettlementInterface: Send + Sync { /// Storage is required for Hyperlane implementation to persist message tracker state /// across restarts. Other implementations may not require storage. pub type SettlementFactory = fn( - &toml::Value, + &serde_json::Value, &NetworksConfig, Arc, ) -> Result, SettlementError>; diff --git a/crates/solver-settlement/src/utils.rs b/crates/solver-settlement/src/utils.rs index ba037f8b..dedefdb1 100644 --- a/crates/solver-settlement/src/utils.rs +++ b/crates/solver-settlement/src/utils.rs @@ -1,26 +1,28 @@ //! Common utilities for settlement implementations. //! //! This module provides shared utilities for parsing oracle configurations -//! from TOML config files, used by all settlement implementations. +//! from JSON config files, used by all settlement implementations. use crate::{OracleConfig, OracleSelectionStrategy, SettlementError}; use solver_types::{utils::parse_address, Address}; use std::collections::HashMap; -/// Parse an oracle table from TOML configuration. +/// Parse an oracle table from JSON configuration. /// /// Parses a table mapping chain IDs to arrays of oracle addresses. /// Expected format: -/// ```toml -/// 31337 = ["0x1111...", "0x2222..."] -/// 31338 = ["0x3333..."] +/// ```json +/// { +/// "31337": ["0x1111...", "0x2222..."], +/// "31338": ["0x3333..."] +/// } /// ``` pub fn parse_oracle_table( - table: &toml::Value, + table: &serde_json::Value, ) -> Result>, SettlementError> { let mut result = HashMap::new(); - if let Some(table) = table.as_table() { + if let Some(table) = table.as_object() { for (chain_id_str, oracles_value) in table { let chain_id = chain_id_str.parse::().map_err(|e| { SettlementError::ValidationFailed(format!("Invalid chain ID '{chain_id_str}': {e}")) @@ -64,18 +66,22 @@ pub fn parse_oracle_table( Ok(result) } -/// Parse a routes table from TOML configuration. +/// Parse a routes table from JSON configuration. /// /// Parses a table mapping source chain IDs to arrays of destination chain IDs. /// Expected format: -/// ```toml -/// 31337 = [31338, 31339] -/// 31338 = [31337] +/// ```json +/// { +/// "31337": [31338, 31339], +/// "31338": [31337] +/// } /// ``` -pub fn parse_routes_table(table: &toml::Value) -> Result>, SettlementError> { +pub fn parse_routes_table( + table: &serde_json::Value, +) -> Result>, SettlementError> { let mut result = HashMap::new(); - if let Some(table) = table.as_table() { + if let Some(table) = table.as_object() { for (chain_id_str, destinations_value) in table { let chain_id = chain_id_str.parse::().map_err(|e| { SettlementError::ValidationFailed(format!("Invalid chain ID '{chain_id_str}': {e}")) @@ -85,7 +91,7 @@ pub fn parse_routes_table(table: &toml::Value) -> Result>, array .iter() .map(|v| { - v.as_integer().map(|i| i as u64).ok_or_else(|| { + v.as_i64().map(|i| i as u64).ok_or_else(|| { SettlementError::ValidationFailed(format!( "Destination chain ID must be integer for route from chain {chain_id}" )) @@ -124,21 +130,23 @@ pub fn parse_selection_strategy(value: Option<&str>) -> OracleSelectionStrategy } } -/// Parse a complete oracle configuration from TOML. +/// Parse a complete oracle configuration from JSON. /// /// Expects a config structure like: -/// ```toml -/// [oracles] -/// input = { 31337 = ["0x..."], 31338 = ["0x..."] } -/// output = { 31337 = ["0x..."], 31338 = ["0x..."] } -/// -/// [routes] -/// 31337 = [31338] -/// 31338 = [31337] -/// -/// oracle_selection_strategy = "RoundRobin" # Optional +/// ```json +/// { +/// "oracles": { +/// "input": { "31337": ["0x..."], "31338": ["0x..."] }, +/// "output": { "31337": ["0x..."], "31338": ["0x..."] } +/// }, +/// "routes": { +/// "31337": [31338], +/// "31338": [31337] +/// }, +/// "oracle_selection_strategy": "RoundRobin" +/// } /// ``` -pub fn parse_oracle_config(config: &toml::Value) -> Result { +pub fn parse_oracle_config(config: &serde_json::Value) -> Result { // Parse oracles section let oracles_table = config.get("oracles").ok_or_else(|| { SettlementError::ValidationFailed("Missing 'oracles' section".to_string()) @@ -232,18 +240,22 @@ mod tests { #[test] fn test_parse_oracle_table_success() { - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "1".to_string(), - toml::Value::Array(vec![ - toml::Value::String("0x1111111111111111111111111111111111111111".to_string()), - toml::Value::String("0x2222222222222222222222222222222222222222".to_string()), + serde_json::Value::Array(vec![ + serde_json::Value::String( + "0x1111111111111111111111111111111111111111".to_string(), + ), + serde_json::Value::String( + "0x2222222222222222222222222222222222222222".to_string(), + ), ]), ); table.insert( "2".to_string(), - toml::Value::Array(vec![toml::Value::String( + serde_json::Value::Array(vec![serde_json::Value::String( "0x3333333333333333333333333333333333333333".to_string(), )]), ); @@ -268,11 +280,11 @@ mod tests { #[test] fn test_parse_oracle_table_invalid_chain_id() { - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "invalid".to_string(), - toml::Value::Array(vec![toml::Value::String( + serde_json::Value::Array(vec![serde_json::Value::String( "0x1111111111111111111111111111111111111111".to_string(), )]), ); @@ -288,11 +300,11 @@ mod tests { #[test] fn test_parse_oracle_table_not_array() { - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "1".to_string(), - toml::Value::String("not_array".to_string()), + serde_json::Value::String("not_array".to_string()), ); table }); @@ -306,9 +318,9 @@ mod tests { #[test] fn test_parse_oracle_table_empty_array() { - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); - table.insert("1".to_string(), toml::Value::Array(vec![])); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); + table.insert("1".to_string(), serde_json::Value::Array(vec![])); table }); @@ -321,11 +333,11 @@ mod tests { #[test] fn test_parse_oracle_table_non_string_address() { - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "1".to_string(), - toml::Value::Array(vec![toml::Value::Integer(123)]), + serde_json::Value::Array(vec![serde_json::Value::from(123)]), ); table }); @@ -339,11 +351,13 @@ mod tests { #[test] fn test_parse_oracle_table_invalid_address() { - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "1".to_string(), - toml::Value::Array(vec![toml::Value::String("invalid_address".to_string())]), + serde_json::Value::Array(vec![serde_json::Value::String( + "invalid_address".to_string(), + )]), ); table }); @@ -357,15 +371,18 @@ mod tests { #[test] fn test_parse_routes_table_success() { - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "1".to_string(), - toml::Value::Array(vec![toml::Value::Integer(2), toml::Value::Integer(3)]), + serde_json::Value::Array(vec![ + serde_json::Value::from(2), + serde_json::Value::from(3), + ]), ); table.insert( "2".to_string(), - toml::Value::Array(vec![toml::Value::Integer(1)]), + serde_json::Value::Array(vec![serde_json::Value::from(1)]), ); table }); @@ -380,11 +397,11 @@ mod tests { #[test] fn test_parse_routes_table_invalid_chain_id() { - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "invalid".to_string(), - toml::Value::Array(vec![toml::Value::Integer(2)]), + serde_json::Value::Array(vec![serde_json::Value::from(2)]), ); table }); @@ -398,9 +415,9 @@ mod tests { #[test] fn test_parse_routes_table_not_array() { - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); - table.insert("1".to_string(), toml::Value::Integer(2)); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); + table.insert("1".to_string(), serde_json::Value::from(2)); table }); @@ -413,9 +430,9 @@ mod tests { #[test] fn test_parse_routes_table_empty_array() { - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); - table.insert("1".to_string(), toml::Value::Array(vec![])); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); + table.insert("1".to_string(), serde_json::Value::Array(vec![])); table }); @@ -428,11 +445,11 @@ mod tests { #[test] fn test_parse_routes_table_non_integer_destination() { - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "1".to_string(), - toml::Value::Array(vec![toml::Value::String("invalid".to_string())]), + serde_json::Value::Array(vec![serde_json::Value::String("invalid".to_string())]), ); table }); @@ -511,19 +528,19 @@ mod tests { #[test] fn test_parse_oracle_config_success() { - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "oracles".to_string(), - toml::Value::Table({ - let mut oracles = toml::map::Map::new(); + serde_json::Value::Object({ + let mut oracles = serde_json::Map::new(); oracles.insert( "input".to_string(), - toml::Value::Table({ - let mut input = toml::map::Map::new(); + serde_json::Value::Object({ + let mut input = serde_json::Map::new(); input.insert( "1".to_string(), - toml::Value::Array(vec![toml::Value::String( + serde_json::Value::Array(vec![serde_json::Value::String( "0x1111111111111111111111111111111111111111".to_string(), )]), ); @@ -532,11 +549,11 @@ mod tests { ); oracles.insert( "output".to_string(), - toml::Value::Table({ - let mut output = toml::map::Map::new(); + serde_json::Value::Object({ + let mut output = serde_json::Map::new(); output.insert( "2".to_string(), - toml::Value::Array(vec![toml::Value::String( + serde_json::Value::Array(vec![serde_json::Value::String( "0x2222222222222222222222222222222222222222".to_string(), )]), ); @@ -548,18 +565,18 @@ mod tests { ); table.insert( "routes".to_string(), - toml::Value::Table({ - let mut routes = toml::map::Map::new(); + serde_json::Value::Object({ + let mut routes = serde_json::Map::new(); routes.insert( "1".to_string(), - toml::Value::Array(vec![toml::Value::Integer(2)]), + serde_json::Value::Array(vec![serde_json::Value::from(2)]), ); routes }), ); table.insert( "oracle_selection_strategy".to_string(), - toml::Value::String("RoundRobin".to_string()), + serde_json::Value::String("RoundRobin".to_string()), ); table }); @@ -576,11 +593,11 @@ mod tests { #[test] fn test_parse_oracle_config_missing_oracles_section() { - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "routes".to_string(), - toml::Value::Table(toml::map::Map::new()), + serde_json::Value::Object(serde_json::Map::new()), ); table }); @@ -594,22 +611,22 @@ mod tests { #[test] fn test_parse_oracle_config_missing_input_oracles() { - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "oracles".to_string(), - toml::Value::Table({ - let mut oracles = toml::map::Map::new(); + serde_json::Value::Object({ + let mut oracles = serde_json::Map::new(); oracles.insert( "output".to_string(), - toml::Value::Table(toml::map::Map::new()), + serde_json::Value::Object(serde_json::Map::new()), ); oracles }), ); table.insert( "routes".to_string(), - toml::Value::Table(toml::map::Map::new()), + serde_json::Value::Object(serde_json::Map::new()), ); table }); @@ -623,22 +640,22 @@ mod tests { #[test] fn test_parse_oracle_config_missing_output_oracles() { - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "oracles".to_string(), - toml::Value::Table({ - let mut oracles = toml::map::Map::new(); + serde_json::Value::Object({ + let mut oracles = serde_json::Map::new(); oracles.insert( "input".to_string(), - toml::Value::Table(toml::map::Map::new()), + serde_json::Value::Object(serde_json::Map::new()), ); oracles }), ); table.insert( "routes".to_string(), - toml::Value::Table(toml::map::Map::new()), + serde_json::Value::Object(serde_json::Map::new()), ); table }); @@ -652,19 +669,19 @@ mod tests { #[test] fn test_parse_oracle_config_missing_routes() { - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "oracles".to_string(), - toml::Value::Table({ - let mut oracles = toml::map::Map::new(); + serde_json::Value::Object({ + let mut oracles = serde_json::Map::new(); oracles.insert( "input".to_string(), - toml::Value::Table(toml::map::Map::new()), + serde_json::Value::Object(serde_json::Map::new()), ); oracles.insert( "output".to_string(), - toml::Value::Table(toml::map::Map::new()), + serde_json::Value::Object(serde_json::Map::new()), ); oracles }), @@ -681,19 +698,19 @@ mod tests { #[test] fn test_parse_oracle_config_default_strategy() { - let config = toml::Value::Table({ - let mut table = toml::map::Map::new(); + let config = serde_json::Value::Object({ + let mut table = serde_json::Map::new(); table.insert( "oracles".to_string(), - toml::Value::Table({ - let mut oracles = toml::map::Map::new(); + serde_json::Value::Object({ + let mut oracles = serde_json::Map::new(); oracles.insert( "input".to_string(), - toml::Value::Table({ - let mut input = toml::map::Map::new(); + serde_json::Value::Object({ + let mut input = serde_json::Map::new(); input.insert( "1".to_string(), - toml::Value::Array(vec![toml::Value::String( + serde_json::Value::Array(vec![serde_json::Value::String( "0x1111111111111111111111111111111111111111".to_string(), )]), ); @@ -702,11 +719,11 @@ mod tests { ); oracles.insert( "output".to_string(), - toml::Value::Table({ - let mut output = toml::map::Map::new(); + serde_json::Value::Object({ + let mut output = serde_json::Map::new(); output.insert( "2".to_string(), - toml::Value::Array(vec![toml::Value::String( + serde_json::Value::Array(vec![serde_json::Value::String( "0x2222222222222222222222222222222222222222".to_string(), )]), ); @@ -718,11 +735,11 @@ mod tests { ); table.insert( "routes".to_string(), - toml::Value::Table({ - let mut routes = toml::map::Map::new(); + serde_json::Value::Object({ + let mut routes = serde_json::Map::new(); routes.insert( "1".to_string(), - toml::Value::Array(vec![toml::Value::Integer(2)]), + serde_json::Value::Array(vec![serde_json::Value::from(2)]), ); routes }), diff --git a/crates/solver-storage/Cargo.toml b/crates/solver-storage/Cargo.toml index ea06927b..c1ca9243 100644 --- a/crates/solver-storage/Cargo.toml +++ b/crates/solver-storage/Cargo.toml @@ -21,7 +21,6 @@ tokio = { version = "1.0", features = [ "sync", "time", ] } -toml = { workspace = true } tracing = { workspace = true } uuid = { workspace = true } diff --git a/crates/solver-storage/src/implementations/file.rs b/crates/solver-storage/src/implementations/file.rs index 31f790a9..2a7716c7 100644 --- a/crates/solver-storage/src/implementations/file.rs +++ b/crates/solver-storage/src/implementations/file.rs @@ -134,15 +134,15 @@ pub struct TtlConfig { impl TtlConfig { /// Creates TTL config from TOML configuration. - fn from_config(config: &toml::Value) -> Self { + fn from_config(config: &serde_json::Value) -> Self { let mut ttls = HashMap::new(); - if let Some(table) = config.as_table() { + if let Some(table) = config.as_object() { for storage_key in StorageKey::all() { let config_key = format!("ttl_{}", storage_key.as_str()); if let Some(ttl_value) = table .get(&config_key) - .and_then(|v| v.as_integer()) + .and_then(|v| v.as_i64()) .map(|v| v as u64) { ttls.insert(storage_key, Duration::from_secs(ttl_value)); @@ -862,14 +862,14 @@ pub struct FileStorageSchema; impl FileStorageSchema { /// Static validation method for use before instance creation - pub fn validate_config(config: &toml::Value) -> Result<(), ValidationError> { + pub fn validate_config(config: &serde_json::Value) -> Result<(), ValidationError> { let instance = Self; instance.validate(config) } } impl ConfigSchema for FileStorageSchema { - fn validate(&self, config: &toml::Value) -> Result<(), ValidationError> { + fn validate(&self, config: &serde_json::Value) -> Result<(), ValidationError> { // Build TTL fields dynamically based on StorageKey variants let mut optional_fields = vec![Field::new("storage_path", FieldType::String)]; @@ -904,7 +904,9 @@ impl ConfigSchema for FileStorageSchema { /// - `ttl_orders`: TTL in seconds for orders (default: 0) /// - `ttl_intents`: TTL in seconds for intents (default: 0) /// - `ttl_order_by_tx_hash`: TTL in seconds for order_by_tx_hash (default: 0) -pub fn create_storage(config: &toml::Value) -> Result, StorageError> { +pub fn create_storage( + config: &serde_json::Value, +) -> Result, StorageError> { // Validate configuration first FileStorageSchema::validate_config(config) .map_err(|e| StorageError::Configuration(format!("Invalid configuration: {e}")))?; @@ -1227,26 +1229,26 @@ mod tests { let schema = FileStorageSchema; // Valid config - let valid_config = toml::Value::Table(toml::toml! { - storage_path = "/tmp/test" - ttl_orders = 3600 - ttl_intents = 7200 + let valid_config = serde_json::json!({ + "storage_path": "/tmp/test", + "ttl_orders": 3600, + "ttl_intents": 7200, }); assert!(schema.validate(&valid_config).is_ok()); // Invalid TTL (negative) - let invalid_config = toml::Value::Table(toml::toml! { - ttl_orders = -1 + let invalid_config = serde_json::json!({ + "ttl_orders": -1, }); assert!(schema.validate(&invalid_config).is_err()); } #[tokio::test] async fn test_ttl_config_from_toml() { - let config = toml::Value::Table(toml::toml! { - ttl_orders = 3600 - ttl_intents = 7200 - ttl_quotes = 1800 + let config = serde_json::json!({ + "ttl_orders": 3600, + "ttl_intents": 7200, + "ttl_quotes": 1800, }); let ttl_config = TtlConfig::from_config(&config); @@ -1271,9 +1273,9 @@ mod tests { #[tokio::test] async fn test_factory_function() { - let config = toml::Value::Table(toml::toml! { - storage_path = "/tmp/test_storage" - ttl_orders = 3600 + let config = serde_json::json!({ + "storage_path": "/tmp/test_storage", + "ttl_orders": 3600, }); let storage = create_storage(&config).unwrap(); diff --git a/crates/solver-storage/src/implementations/memory.rs b/crates/solver-storage/src/implementations/memory.rs index 30756c66..aeac2dbe 100644 --- a/crates/solver-storage/src/implementations/memory.rs +++ b/crates/solver-storage/src/implementations/memory.rs @@ -14,7 +14,8 @@ use crate::{QueryFilter, StorageError, StorageIndexes, StorageInterface}; use async_trait::async_trait; -use solver_types::{ConfigSchema, Schema, ValidationError}; +use serde::Deserialize; +use solver_types::{ConfigSchema, ValidationError}; use std::collections::HashMap; use std::sync::Arc; use std::time::{Duration, Instant}; @@ -217,19 +218,28 @@ impl StorageInterface for MemoryStorage { /// Configuration schema for MemoryStorage. pub struct MemoryStorageSchema; +/// Dedicated typed configuration for in-memory storage. +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct MemoryStorageConfig {} + +impl MemoryStorageConfig { + fn from_json(config: &serde_json::Value) -> Result { + serde_json::from_value(config.clone()) + .map_err(|err| ValidationError::DeserializationError(err.to_string())) + } +} + impl MemoryStorageSchema { /// Static validation method for use before instance creation - pub fn validate_config(config: &toml::Value) -> Result<(), ValidationError> { - let instance = Self; - instance.validate(config) + pub fn validate_config(config: &serde_json::Value) -> Result<(), ValidationError> { + MemoryStorageConfig::from_json(config).map(|_| ()) } } impl ConfigSchema for MemoryStorageSchema { - fn validate(&self, _config: &toml::Value) -> Result<(), ValidationError> { - // Memory storage has no required configuration - let schema = Schema::new(vec![], vec![]); - schema.validate(_config) + fn validate(&self, config: &serde_json::Value) -> Result<(), ValidationError> { + MemoryStorageConfig::from_json(config).map(|_| ()) } } @@ -237,7 +247,9 @@ impl ConfigSchema for MemoryStorageSchema { /// /// Configuration parameters: /// - None required for memory storage -pub fn create_storage(config: &toml::Value) -> Result, StorageError> { +pub fn create_storage( + config: &serde_json::Value, +) -> Result, StorageError> { // Validate configuration first (even though memory storage has no config) MemoryStorageSchema::validate_config(config) .map_err(|e| StorageError::Configuration(format!("Invalid configuration: {e}")))?; @@ -261,6 +273,26 @@ impl solver_types::ImplementationRegistry for Registry { mod tests { use super::*; + #[test] + fn test_schema_validation_valid_empty_config() { + let config = serde_json::Value::Object(serde_json::Map::new()); + assert!(MemoryStorageSchema::validate_config(&config).is_ok()); + } + + #[test] + fn test_schema_validation_rejects_unknown_fields() { + let config = serde_json::json!({ + "unexpected": "value" + }); + assert!(MemoryStorageSchema::validate_config(&config).is_err()); + } + + #[test] + fn test_schema_validation_rejects_non_table_root() { + let config = serde_json::Value::String("not-a-table".to_string()); + assert!(MemoryStorageSchema::validate_config(&config).is_err()); + } + #[tokio::test] async fn test_basic_operations() { let storage = MemoryStorage::new(); diff --git a/crates/solver-storage/src/implementations/redis.rs b/crates/solver-storage/src/implementations/redis.rs index c9630967..08a2df78 100644 --- a/crates/solver-storage/src/implementations/redis.rs +++ b/crates/solver-storage/src/implementations/redis.rs @@ -14,23 +14,28 @@ //! //! # Configuration //! -//! The Redis storage backend is configured via TOML: +//! The Redis storage backend is configured via JSON: //! -//! ```toml -//! [storage] -//! primary = "redis" -//! cleanup_interval_seconds = 60 -//! -//! [storage.implementations.redis] -//! redis_url = "redis://localhost:6379" -//! key_prefix = "oif-solver" -//! connection_timeout_ms = 5000 -//! db = 0 -//! ttl_orders = 300 # 5 minutes -//! ttl_intents = 120 # 2 minutes -//! ttl_order_by_tx_hash = 300 # 5 minutes -//! ttl_quotes = 60 # 1 minute -//! ttl_settlement_messages = 600 # 10 minutes +//! ```json +//! { +//! "storage": { +//! "primary": "redis", +//! "cleanup_interval_seconds": 60, +//! "implementations": { +//! "redis": { +//! "redis_url": "redis://localhost:6379", +//! "key_prefix": "oif-solver", +//! "connection_timeout_ms": 5000, +//! "db": 0, +//! "ttl_orders": 300, +//! "ttl_intents": 120, +//! "ttl_order_by_tx_hash": 300, +//! "ttl_quotes": 60, +//! "ttl_settlement_messages": 600 +//! } +//! } +//! } +//! } //! ``` //! //! ## Configuration Options @@ -122,15 +127,15 @@ impl TtlConfig { } /// Creates TTL config from TOML configuration. - fn from_config(config: &toml::Value) -> Self { + fn from_config(config: &serde_json::Value) -> Self { let mut ttls = HashMap::new(); - if let Some(table) = config.as_table() { + if let Some(table) = config.as_object() { for storage_key in StorageKey::all() { let config_key = format!("ttl_{}", storage_key.as_str()); if let Some(ttl_value) = table .get(&config_key) - .and_then(|v| v.as_integer()) + .and_then(|v| v.as_i64()) .map(|v| v as u64) { if ttl_value > 0 { @@ -836,14 +841,14 @@ pub struct RedisStorageSchema; impl RedisStorageSchema { /// Static validation method for use before instance creation. - pub fn validate_config(config: &toml::Value) -> Result<(), ValidationError> { + pub fn validate_config(config: &serde_json::Value) -> Result<(), ValidationError> { let instance = Self; instance.validate(config) } } impl ConfigSchema for RedisStorageSchema { - fn validate(&self, config: &toml::Value) -> Result<(), ValidationError> { + fn validate(&self, config: &serde_json::Value) -> Result<(), ValidationError> { // Build TTL fields dynamically based on StorageKey variants // Note: redis_url is required, so it's not in optional_fields let mut optional_fields = vec![ @@ -954,7 +959,9 @@ fn build_redis_url(redis_url: &str, db: u8) -> String { /// - `ttl_order_by_tx_hash`: TTL in seconds for order_by_tx_hash mappings (default: no expiration) /// - `ttl_quotes`: TTL in seconds for quotes (default: no expiration) /// - `ttl_settlement_messages`: TTL in seconds for settlement messages (default: no expiration) -pub fn create_storage(config: &toml::Value) -> Result, StorageError> { +pub fn create_storage( + config: &serde_json::Value, +) -> Result, StorageError> { // Validate configuration first RedisStorageSchema::validate_config(config) .map_err(|e| StorageError::Configuration(format!("Invalid configuration: {e}")))?; @@ -972,13 +979,13 @@ pub fn create_storage(config: &toml::Value) -> Result, let timeout_ms = config .get("connection_timeout_ms") - .and_then(|v| v.as_integer()) + .and_then(|v| v.as_i64()) .map(|v| v as u64) .unwrap_or(DEFAULT_CONNECTION_TIMEOUT_MS); let db = config .get("db") - .and_then(|v| v.as_integer()) + .and_then(|v| v.as_i64()) .map(|v| v as u8) .unwrap_or(0); @@ -1000,7 +1007,7 @@ pub fn create_storage(config: &toml::Value) -> Result, /// Use this when you need to create storage in an async context without blocking. /// This function also eagerly initializes the connection to verify connectivity. pub async fn create_storage_async( - config: &toml::Value, + config: &serde_json::Value, ) -> Result, StorageError> { // Validate configuration first RedisStorageSchema::validate_config(config) @@ -1019,13 +1026,13 @@ pub async fn create_storage_async( let timeout_ms = config .get("connection_timeout_ms") - .and_then(|v| v.as_integer()) + .and_then(|v| v.as_i64()) .map(|v| v as u64) .unwrap_or(DEFAULT_CONNECTION_TIMEOUT_MS); let db = config .get("db") - .and_then(|v| v.as_integer()) + .and_then(|v| v.as_i64()) .map(|v| v as u8) .unwrap_or(0); @@ -1061,11 +1068,11 @@ mod tests { #[test] fn test_ttl_config_from_toml() { - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://localhost:6379" - ttl_orders = 3600 - ttl_intents = 7200 - ttl_quotes = 1800 + let config = serde_json::json!({ + "redis_url": "redis://localhost:6379", + "ttl_orders": 3600, + "ttl_intents": 7200, + "ttl_quotes": 1800, }); let ttl_config = TtlConfig::from_config(&config); @@ -1087,10 +1094,10 @@ mod tests { #[test] fn test_ttl_config_zero_values() { - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://localhost:6379" - ttl_orders = 0 - ttl_intents = 100 + let config = serde_json::json!({ + "redis_url": "redis://localhost:6379", + "ttl_orders": 0, + "ttl_intents": 100, }); let ttl_config = TtlConfig::from_config(&config); @@ -1105,7 +1112,7 @@ mod tests { #[test] fn test_ttl_config_empty_config() { - let config = toml::Value::Table(toml::map::Map::new()); + let config = serde_json::Value::Object(serde_json::Map::new()); let ttl_config = TtlConfig::from_config(&config); // No TTLs should be configured @@ -1119,7 +1126,7 @@ mod tests { #[test] fn test_ttl_config_non_table_value() { // When config is not a table, should return empty TtlConfig - let config = toml::Value::String("not a table".to_string()); + let config = serde_json::Value::String("not a table".to_string()); let ttl_config = TtlConfig::from_config(&config); assert_eq!(ttl_config.get_ttl(StorageKey::Orders), None); @@ -1127,12 +1134,12 @@ mod tests { #[test] fn test_ttl_config_all_storage_keys() { - let config = toml::Value::Table(toml::toml! { - ttl_orders = 100 - ttl_intents = 200 - ttl_order_by_tx_hash = 300 - ttl_quotes = 400 - ttl_settlement_messages = 500 + let config = serde_json::json!({ + "ttl_orders": 100, + "ttl_intents": 200, + "ttl_order_by_tx_hash": 300, + "ttl_quotes": 400, + "ttl_settlement_messages": 500, }); let ttl_config = TtlConfig::from_config(&config); @@ -1163,8 +1170,8 @@ mod tests { fn test_ttl_config_negative_values_treated_as_zero() { // Negative values in TOML are still i64, but we cast to u64 // This tests the behavior with edge values - let config = toml::Value::Table(toml::toml! { - ttl_orders = 1 + let config = serde_json::json!({ + "ttl_orders": 1, }); let ttl_config = TtlConfig::from_config(&config); @@ -1176,8 +1183,8 @@ mod tests { #[test] fn test_ttl_config_debug_impl() { - let config = toml::Value::Table(toml::toml! { - ttl_orders = 100 + let config = serde_json::json!({ + "ttl_orders": 100, }); let ttl_config = TtlConfig::from_config(&config); @@ -1191,7 +1198,7 @@ mod tests { #[test] fn test_redis_storage_new_valid() { - let ttl_config = TtlConfig::from_config(&toml::Value::Table(toml::map::Map::new())); + let ttl_config = TtlConfig::from_config(&serde_json::Value::Object(serde_json::Map::new())); let result = RedisStorage::new( "redis://localhost:6379".to_string(), 5000, @@ -1208,7 +1215,7 @@ mod tests { #[test] fn test_redis_storage_new_empty_prefix() { - let ttl_config = TtlConfig::from_config(&toml::Value::Table(toml::map::Map::new())); + let ttl_config = TtlConfig::from_config(&serde_json::Value::Object(serde_json::Map::new())); let result = RedisStorage::new( "redis://localhost:6379".to_string(), 5000, @@ -1224,7 +1231,7 @@ mod tests { #[test] fn test_redis_storage_new_empty_url() { - let ttl_config = TtlConfig::from_config(&toml::Value::Table(toml::map::Map::new())); + let ttl_config = TtlConfig::from_config(&serde_json::Value::Object(serde_json::Map::new())); let result = RedisStorage::new("".to_string(), 5000, "test".to_string(), ttl_config); assert!(result.is_err()); @@ -1237,7 +1244,7 @@ mod tests { #[test] fn test_data_key_generation() { - let ttl_config = TtlConfig::from_config(&toml::Value::Table(toml::map::Map::new())); + let ttl_config = TtlConfig::from_config(&serde_json::Value::Object(serde_json::Map::new())); let storage = RedisStorage::new( "redis://localhost:6379".to_string(), 5000, @@ -1256,7 +1263,7 @@ mod tests { #[test] fn test_data_key_with_custom_prefix() { - let ttl_config = TtlConfig::from_config(&toml::Value::Table(toml::map::Map::new())); + let ttl_config = TtlConfig::from_config(&serde_json::Value::Object(serde_json::Map::new())); let storage = RedisStorage::new( "redis://localhost:6379".to_string(), 5000, @@ -1273,7 +1280,7 @@ mod tests { #[test] fn test_all_ids_key_generation() { - let ttl_config = TtlConfig::from_config(&toml::Value::Table(toml::map::Map::new())); + let ttl_config = TtlConfig::from_config(&serde_json::Value::Object(serde_json::Map::new())); let storage = RedisStorage::new( "redis://localhost:6379".to_string(), 5000, @@ -1289,7 +1296,7 @@ mod tests { #[test] fn test_index_key_generation_string_value() { - let ttl_config = TtlConfig::from_config(&toml::Value::Table(toml::map::Map::new())); + let ttl_config = TtlConfig::from_config(&serde_json::Value::Object(serde_json::Map::new())); let storage = RedisStorage::new( "redis://localhost:6379".to_string(), 5000, @@ -1307,7 +1314,7 @@ mod tests { #[test] fn test_index_key_generation_number_value() { - let ttl_config = TtlConfig::from_config(&toml::Value::Table(toml::map::Map::new())); + let ttl_config = TtlConfig::from_config(&serde_json::Value::Object(serde_json::Map::new())); let storage = RedisStorage::new( "redis://localhost:6379".to_string(), 5000, @@ -1325,7 +1332,7 @@ mod tests { #[test] fn test_index_key_generation_bool_value() { - let ttl_config = TtlConfig::from_config(&toml::Value::Table(toml::map::Map::new())); + let ttl_config = TtlConfig::from_config(&serde_json::Value::Object(serde_json::Map::new())); let storage = RedisStorage::new( "redis://localhost:6379".to_string(), 5000, @@ -1349,7 +1356,7 @@ mod tests { #[test] fn test_index_key_generation_complex_value() { - let ttl_config = TtlConfig::from_config(&toml::Value::Table(toml::map::Map::new())); + let ttl_config = TtlConfig::from_config(&serde_json::Value::Object(serde_json::Map::new())); let storage = RedisStorage::new( "redis://localhost:6379".to_string(), 5000, @@ -1372,7 +1379,7 @@ mod tests { #[test] fn test_index_key_generation_null_value() { - let ttl_config = TtlConfig::from_config(&toml::Value::Table(toml::map::Map::new())); + let ttl_config = TtlConfig::from_config(&serde_json::Value::Object(serde_json::Map::new())); let storage = RedisStorage::new( "redis://localhost:6379".to_string(), 5000, @@ -1390,7 +1397,7 @@ mod tests { #[test] fn test_index_meta_key_generation() { - let ttl_config = TtlConfig::from_config(&toml::Value::Table(toml::map::Map::new())); + let ttl_config = TtlConfig::from_config(&serde_json::Value::Object(serde_json::Map::new())); let storage = RedisStorage::new( "redis://localhost:6379".to_string(), 5000, @@ -1411,7 +1418,7 @@ mod tests { #[test] fn test_index_meta_key_with_custom_prefix() { - let ttl_config = TtlConfig::from_config(&toml::Value::Table(toml::map::Map::new())); + let ttl_config = TtlConfig::from_config(&serde_json::Value::Object(serde_json::Map::new())); let storage = RedisStorage::new( "redis://localhost:6379".to_string(), 5000, @@ -1430,8 +1437,8 @@ mod tests { #[test] fn test_get_ttl_for_key_orders() { - let config = toml::Value::Table(toml::toml! { - ttl_orders = 3600 + let config = serde_json::json!({ + "ttl_orders": 3600, }); let ttl_config = TtlConfig::from_config(&config); let storage = RedisStorage::new( @@ -1450,8 +1457,8 @@ mod tests { #[test] fn test_get_ttl_for_key_intents() { - let config = toml::Value::Table(toml::toml! { - ttl_intents = 1800 + let config = serde_json::json!({ + "ttl_intents": 1800, }); let ttl_config = TtlConfig::from_config(&config); let storage = RedisStorage::new( @@ -1470,8 +1477,8 @@ mod tests { #[test] fn test_get_ttl_for_key_no_ttl_configured() { - let config = toml::Value::Table(toml::toml! { - ttl_orders = 3600 + let config = serde_json::json!({ + "ttl_orders": 3600, }); let ttl_config = TtlConfig::from_config(&config); let storage = RedisStorage::new( @@ -1488,8 +1495,8 @@ mod tests { #[test] fn test_get_ttl_for_key_unknown_namespace() { - let config = toml::Value::Table(toml::toml! { - ttl_orders = 3600 + let config = serde_json::json!({ + "ttl_orders": 3600, }); let ttl_config = TtlConfig::from_config(&config); let storage = RedisStorage::new( @@ -1506,8 +1513,8 @@ mod tests { #[test] fn test_get_ttl_for_key_empty_key() { - let config = toml::Value::Table(toml::toml! { - ttl_orders = 3600 + let config = serde_json::json!({ + "ttl_orders": 3600, }); let ttl_config = TtlConfig::from_config(&config); let storage = RedisStorage::new( @@ -1523,8 +1530,8 @@ mod tests { #[test] fn test_get_ttl_for_key_no_colon() { - let config = toml::Value::Table(toml::toml! { - ttl_orders = 3600 + let config = serde_json::json!({ + "ttl_orders": 3600, }); let ttl_config = TtlConfig::from_config(&config); let storage = RedisStorage::new( @@ -1546,7 +1553,7 @@ mod tests { #[test] fn test_map_redis_error_type_error() { - let ttl_config = TtlConfig::from_config(&toml::Value::Table(toml::map::Map::new())); + let ttl_config = TtlConfig::from_config(&serde_json::Value::Object(serde_json::Map::new())); let storage = RedisStorage::new( "redis://localhost:6379".to_string(), 5000, @@ -1566,7 +1573,7 @@ mod tests { #[test] fn test_map_redis_error_auth_failed() { - let ttl_config = TtlConfig::from_config(&toml::Value::Table(toml::map::Map::new())); + let ttl_config = TtlConfig::from_config(&serde_json::Value::Object(serde_json::Map::new())); let storage = RedisStorage::new( "redis://localhost:6379".to_string(), 5000, @@ -1585,7 +1592,7 @@ mod tests { #[test] fn test_map_redis_error_io_error() { - let ttl_config = TtlConfig::from_config(&toml::Value::Table(toml::map::Map::new())); + let ttl_config = TtlConfig::from_config(&serde_json::Value::Object(serde_json::Map::new())); let storage = RedisStorage::new( "redis://localhost:6379".to_string(), 5000, @@ -1605,7 +1612,7 @@ mod tests { #[test] fn test_map_redis_error_generic() { - let ttl_config = TtlConfig::from_config(&toml::Value::Table(toml::map::Map::new())); + let ttl_config = TtlConfig::from_config(&serde_json::Value::Object(serde_json::Map::new())); let storage = RedisStorage::new( "redis://localhost:6379".to_string(), 5000, @@ -1630,12 +1637,12 @@ mod tests { fn test_config_schema_validation_valid() { let schema = RedisStorageSchema; - let valid_config = toml::Value::Table(toml::toml! { - redis_url = "redis://localhost:6379" - key_prefix = "test" - connection_timeout_ms = 5000 - db = 0 - ttl_orders = 3600 + let valid_config = serde_json::json!({ + "redis_url": "redis://localhost:6379", + "key_prefix": "test", + "connection_timeout_ms": 5000, + "db": 0, + "ttl_orders": 3600, }); assert!(schema.validate(&valid_config).is_ok()); @@ -1645,8 +1652,8 @@ mod tests { fn test_config_schema_validation_missing_url() { let schema = RedisStorageSchema; - let invalid_config = toml::Value::Table(toml::toml! { - key_prefix = "test" + let invalid_config = serde_json::json!({ + "key_prefix": "test", }); assert!(schema.validate(&invalid_config).is_err()); @@ -1657,9 +1664,9 @@ mod tests { let schema = RedisStorageSchema; // Timeout too low - let invalid_config = toml::Value::Table(toml::toml! { - redis_url = "redis://localhost:6379" - connection_timeout_ms = 10 + let invalid_config = serde_json::json!({ + "redis_url": "redis://localhost:6379", + "connection_timeout_ms": 10, }); assert!(schema.validate(&invalid_config).is_err()); @@ -1670,9 +1677,9 @@ mod tests { let schema = RedisStorageSchema; // Timeout too high (> 60000) - let invalid_config = toml::Value::Table(toml::toml! { - redis_url = "redis://localhost:6379" - connection_timeout_ms = 100000 + let invalid_config = serde_json::json!({ + "redis_url": "redis://localhost:6379", + "connection_timeout_ms": 100000, }); assert!(schema.validate(&invalid_config).is_err()); @@ -1683,9 +1690,9 @@ mod tests { let schema = RedisStorageSchema; // DB out of range (> 15) - let invalid_config = toml::Value::Table(toml::toml! { - redis_url = "redis://localhost:6379" - db = 20 + let invalid_config = serde_json::json!({ + "redis_url": "redis://localhost:6379", + "db": 20, }); assert!(schema.validate(&invalid_config).is_err()); @@ -1696,9 +1703,9 @@ mod tests { let schema = RedisStorageSchema; // DB negative - let invalid_config = toml::Value::Table(toml::toml! { - redis_url = "redis://localhost:6379" - db = -1 + let invalid_config = serde_json::json!({ + "redis_url": "redis://localhost:6379", + "db": -1, }); assert!(schema.validate(&invalid_config).is_err()); @@ -1709,8 +1716,8 @@ mod tests { let schema = RedisStorageSchema; // Only redis_url is required - let minimal_config = toml::Value::Table(toml::toml! { - redis_url = "redis://localhost:6379" + let minimal_config = serde_json::json!({ + "redis_url": "redis://localhost:6379", }); assert!(schema.validate(&minimal_config).is_ok()); @@ -1720,13 +1727,13 @@ mod tests { fn test_config_schema_validation_all_ttls() { let schema = RedisStorageSchema; - let config_with_all_ttls = toml::Value::Table(toml::toml! { - redis_url = "redis://localhost:6379" - ttl_orders = 100 - ttl_intents = 200 - ttl_order_by_tx_hash = 300 - ttl_quotes = 400 - ttl_settlement_messages = 500 + let config_with_all_ttls = serde_json::json!({ + "redis_url": "redis://localhost:6379", + "ttl_orders": 100, + "ttl_intents": 200, + "ttl_order_by_tx_hash": 300, + "ttl_quotes": 400, + "ttl_settlement_messages": 500, }); assert!(schema.validate(&config_with_all_ttls).is_ok()); @@ -1734,13 +1741,13 @@ mod tests { #[test] fn test_config_schema_static_validate() { - let valid_config = toml::Value::Table(toml::toml! { - redis_url = "redis://localhost:6379" + let valid_config = serde_json::json!({ + "redis_url": "redis://localhost:6379", }); assert!(RedisStorageSchema::validate_config(&valid_config).is_ok()); - let invalid_config = toml::Value::Table(toml::map::Map::new()); + let invalid_config = serde_json::Value::Object(serde_json::Map::new()); assert!(RedisStorageSchema::validate_config(&invalid_config).is_err()); } @@ -1748,10 +1755,10 @@ mod tests { #[test] fn test_create_storage_valid_config() { - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://localhost:6379" - key_prefix = "test" - connection_timeout_ms = 5000 + let config = serde_json::json!({ + "redis_url": "redis://localhost:6379", + "key_prefix": "test", + "connection_timeout_ms": 5000, }); let result = create_storage(&config); @@ -1760,8 +1767,8 @@ mod tests { #[test] fn test_create_storage_default_prefix() { - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://localhost:6379" + let config = serde_json::json!({ + "redis_url": "redis://localhost:6379", }); let result = create_storage(&config); @@ -1770,8 +1777,8 @@ mod tests { #[test] fn test_create_storage_default_timeout() { - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://localhost:6379" + let config = serde_json::json!({ + "redis_url": "redis://localhost:6379", }); let result = create_storage(&config); @@ -1780,9 +1787,9 @@ mod tests { #[test] fn test_create_storage_with_db_number() { - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://localhost:6379" - db = 5 + let config = serde_json::json!({ + "redis_url": "redis://localhost:6379", + "db": 5, }); let result = create_storage(&config); @@ -1791,9 +1798,9 @@ mod tests { #[test] fn test_create_storage_url_already_has_db() { - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://localhost:6379/3" - db = 5 + let config = serde_json::json!({ + "redis_url": "redis://localhost:6379/3", + "db": 5, }); // Should use the db from URL, not from config @@ -1803,8 +1810,8 @@ mod tests { #[test] fn test_create_storage_missing_url() { - let config = toml::Value::Table(toml::toml! { - key_prefix = "test" + let config = serde_json::json!({ + "key_prefix": "test", }); let result = create_storage(&config); @@ -1817,9 +1824,9 @@ mod tests { #[test] fn test_create_storage_invalid_timeout() { - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://localhost:6379" - connection_timeout_ms = 10 + let config = serde_json::json!({ + "redis_url": "redis://localhost:6379", + "connection_timeout_ms": 10, }); let result = create_storage(&config); @@ -1828,10 +1835,10 @@ mod tests { #[test] fn test_create_storage_with_ttl_config() { - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://localhost:6379" - ttl_orders = 3600 - ttl_intents = 1800 + let config = serde_json::json!({ + "redis_url": "redis://localhost:6379", + "ttl_orders": 3600, + "ttl_intents": 1800, }); let result = create_storage(&config); @@ -1840,9 +1847,9 @@ mod tests { #[test] fn test_create_storage_url_with_trailing_slash() { - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://localhost:6379/" - db = 2 + let config = serde_json::json!({ + "redis_url": "redis://localhost:6379/", + "db": 2, }); let result = create_storage(&config); @@ -1853,7 +1860,7 @@ mod tests { #[test] fn test_redis_storage_debug() { - let ttl_config = TtlConfig::from_config(&toml::Value::Table(toml::map::Map::new())); + let ttl_config = TtlConfig::from_config(&serde_json::Value::Object(serde_json::Map::new())); let storage = RedisStorage::new( "redis://localhost:6379".to_string(), 5000, @@ -1881,8 +1888,8 @@ mod tests { #[test] fn test_registry_factory() { let factory = Registry::factory(); - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://localhost:6379" + let config = serde_json::json!({ + "redis_url": "redis://localhost:6379", }); let result = factory(&config); @@ -1904,7 +1911,7 @@ mod tests { #[test] fn test_storage_interface_config_schema() { - let ttl_config = TtlConfig::from_config(&toml::Value::Table(toml::map::Map::new())); + let ttl_config = TtlConfig::from_config(&serde_json::Value::Object(serde_json::Map::new())); let storage = RedisStorage::new( "redis://localhost:6379".to_string(), 5000, @@ -1916,12 +1923,12 @@ mod tests { let schema = storage.config_schema(); // Test that the schema validates correctly - let valid_config = toml::Value::Table(toml::toml! { - redis_url = "redis://localhost:6379" + let valid_config = serde_json::json!({ + "redis_url": "redis://localhost:6379", }); assert!(schema.validate(&valid_config).is_ok()); - let invalid_config = toml::Value::Table(toml::map::Map::new()); + let invalid_config = serde_json::Value::Object(serde_json::Map::new()); assert!(schema.validate(&invalid_config).is_err()); } @@ -1930,9 +1937,9 @@ mod tests { #[test] fn test_url_with_path_not_db() { // URL that has a slash but the last segment is not a valid db number - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://localhost:6379/notanumber" - db = 5 + let config = serde_json::json!({ + "redis_url": "redis://localhost:6379/notanumber", + "db": 5, }); // This should append db because "notanumber" is not a valid u8 @@ -1943,9 +1950,9 @@ mod tests { #[test] fn test_url_building_db_out_of_u8_range() { // URL with a number larger than u8 max - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://localhost:6379/256" - db = 5 + let config = serde_json::json!({ + "redis_url": "redis://localhost:6379/256", + "db": 5, }); // 256 is out of u8 range, so should append db @@ -1990,9 +1997,9 @@ mod tests { #[tokio::test] async fn test_get_bytes_connection_failure() { - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://invalid-host:6379" - connection_timeout_ms = 100 + let config = serde_json::json!({ + "redis_url": "redis://invalid-host:6379", + "connection_timeout_ms": 100, }); let storage = create_storage(&config).unwrap(); @@ -2004,9 +2011,9 @@ mod tests { #[tokio::test] async fn test_set_bytes_connection_failure() { - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://invalid-host:6379" - connection_timeout_ms = 100 + let config = serde_json::json!({ + "redis_url": "redis://invalid-host:6379", + "connection_timeout_ms": 100, }); let storage = create_storage(&config).unwrap(); @@ -2020,9 +2027,9 @@ mod tests { #[tokio::test] async fn test_set_bytes_with_indexes_connection_failure() { - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://invalid-host:6379" - connection_timeout_ms = 100 + let config = serde_json::json!({ + "redis_url": "redis://invalid-host:6379", + "connection_timeout_ms": 100, }); let storage = create_storage(&config).unwrap(); @@ -2037,9 +2044,9 @@ mod tests { #[tokio::test] async fn test_set_bytes_with_explicit_ttl_connection_failure() { - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://invalid-host:6379" - connection_timeout_ms = 100 + let config = serde_json::json!({ + "redis_url": "redis://invalid-host:6379", + "connection_timeout_ms": 100, }); let storage = create_storage(&config).unwrap(); @@ -2058,9 +2065,9 @@ mod tests { #[tokio::test] async fn test_delete_connection_failure() { - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://invalid-host:6379" - connection_timeout_ms = 100 + let config = serde_json::json!({ + "redis_url": "redis://invalid-host:6379", + "connection_timeout_ms": 100, }); let storage = create_storage(&config).unwrap(); @@ -2072,9 +2079,9 @@ mod tests { #[tokio::test] async fn test_exists_connection_failure() { - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://invalid-host:6379" - connection_timeout_ms = 100 + let config = serde_json::json!({ + "redis_url": "redis://invalid-host:6379", + "connection_timeout_ms": 100, }); let storage = create_storage(&config).unwrap(); @@ -2086,9 +2093,9 @@ mod tests { #[tokio::test] async fn test_query_all_connection_failure() { - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://invalid-host:6379" - connection_timeout_ms = 100 + let config = serde_json::json!({ + "redis_url": "redis://invalid-host:6379", + "connection_timeout_ms": 100, }); let storage = create_storage(&config).unwrap(); @@ -2100,9 +2107,9 @@ mod tests { #[tokio::test] async fn test_query_equals_connection_failure() { - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://invalid-host:6379" - connection_timeout_ms = 100 + let config = serde_json::json!({ + "redis_url": "redis://invalid-host:6379", + "connection_timeout_ms": 100, }); let storage = create_storage(&config).unwrap(); @@ -2119,9 +2126,9 @@ mod tests { #[tokio::test] async fn test_query_not_equals_connection_failure() { - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://invalid-host:6379" - connection_timeout_ms = 100 + let config = serde_json::json!({ + "redis_url": "redis://invalid-host:6379", + "connection_timeout_ms": 100, }); let storage = create_storage(&config).unwrap(); @@ -2138,9 +2145,9 @@ mod tests { #[tokio::test] async fn test_query_in_connection_failure() { - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://invalid-host:6379" - connection_timeout_ms = 100 + let config = serde_json::json!({ + "redis_url": "redis://invalid-host:6379", + "connection_timeout_ms": 100, }); let storage = create_storage(&config).unwrap(); @@ -2160,9 +2167,9 @@ mod tests { #[tokio::test] async fn test_query_not_in_connection_failure() { - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://invalid-host:6379" - connection_timeout_ms = 100 + let config = serde_json::json!({ + "redis_url": "redis://invalid-host:6379", + "connection_timeout_ms": 100, }); let storage = create_storage(&config).unwrap(); @@ -2182,9 +2189,9 @@ mod tests { #[tokio::test] async fn test_get_batch_connection_failure() { - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://invalid-host:6379" - connection_timeout_ms = 100 + let config = serde_json::json!({ + "redis_url": "redis://invalid-host:6379", + "connection_timeout_ms": 100, }); let storage = create_storage(&config).unwrap(); @@ -2197,9 +2204,9 @@ mod tests { #[tokio::test] async fn test_get_batch_empty_keys() { - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://invalid-host:6379" - connection_timeout_ms = 100 + let config = serde_json::json!({ + "redis_url": "redis://invalid-host:6379", + "connection_timeout_ms": 100, }); let storage = create_storage(&config).unwrap(); @@ -2213,9 +2220,9 @@ mod tests { #[tokio::test] async fn test_cleanup_expired_no_connection_needed() { - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://invalid-host:6379" - connection_timeout_ms = 100 + let config = serde_json::json!({ + "redis_url": "redis://invalid-host:6379", + "connection_timeout_ms": 100, }); let storage = create_storage(&config).unwrap(); @@ -2243,8 +2250,8 @@ mod tests { #[tokio::test] async fn test_create_storage_async_validation_failure() { - let invalid_config = toml::Value::Table(toml::toml! { - key_prefix = "test" + let invalid_config = serde_json::json!({ + "key_prefix": "test", }); let result = create_storage_async(&invalid_config).await; @@ -2254,9 +2261,9 @@ mod tests { #[tokio::test] async fn test_create_storage_async_connection_failure() { - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://invalid-host:6379" - connection_timeout_ms = 100 + let config = serde_json::json!({ + "redis_url": "redis://invalid-host:6379", + "connection_timeout_ms": 100, }); let result = create_storage_async(&config).await; @@ -2268,9 +2275,9 @@ mod tests { #[tokio::test] async fn test_update_indexes_connection_failure() { - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://invalid-host:6379" - connection_timeout_ms = 100 + let config = serde_json::json!({ + "redis_url": "redis://invalid-host:6379", + "connection_timeout_ms": 100, }); let ttl_config = TtlConfig::from_config(&config); @@ -2293,9 +2300,9 @@ mod tests { #[tokio::test] async fn test_update_indexes_with_ttl_connection_failure() { - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://invalid-host:6379" - connection_timeout_ms = 100 + let config = serde_json::json!({ + "redis_url": "redis://invalid-host:6379", + "connection_timeout_ms": 100, }); let ttl_config = TtlConfig::from_config(&config); @@ -2325,9 +2332,9 @@ mod tests { #[tokio::test] async fn test_remove_from_indexes_connection_failure() { - let config = toml::Value::Table(toml::toml! { - redis_url = "redis://invalid-host:6379" - connection_timeout_ms = 100 + let config = serde_json::json!({ + "redis_url": "redis://invalid-host:6379", + "connection_timeout_ms": 100, }); let ttl_config = TtlConfig::from_config(&config); @@ -2349,7 +2356,7 @@ mod tests { #[tokio::test] async fn test_get_connection_lazy_initialization() { - let ttl_config = TtlConfig::from_config(&toml::Value::Table(toml::map::Map::new())); + let ttl_config = TtlConfig::from_config(&serde_json::Value::Object(serde_json::Map::new())); let storage = RedisStorage::new( "redis://invalid-host:6379".to_string(), 100, diff --git a/crates/solver-storage/src/lib.rs b/crates/solver-storage/src/lib.rs index 037df7d5..b0e7d9e9 100644 --- a/crates/solver-storage/src/lib.rs +++ b/crates/solver-storage/src/lib.rs @@ -255,7 +255,7 @@ pub trait StorageInterface: Send + Sync { /// /// This is the function signature that all storage implementations must provide /// to create instances of their storage interface. -pub type StorageFactory = fn(&toml::Value) -> Result, StorageError>; +pub type StorageFactory = fn(&serde_json::Value) -> Result, StorageError>; /// Get all registered storage implementations. /// diff --git a/crates/solver-types/Cargo.toml b/crates/solver-types/Cargo.toml index 74975a0a..799bd70b 100644 --- a/crates/solver-types/Cargo.toml +++ b/crates/solver-types/Cargo.toml @@ -27,6 +27,5 @@ serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true, features = ["sync"] } -toml = { workspace = true } tracing = "0.1" zeroize = { version = "1.8", features = ["derive"] } diff --git a/crates/solver-types/src/validation.rs b/crates/solver-types/src/validation.rs index 42da717a..db1a9695 100644 --- a/crates/solver-types/src/validation.rs +++ b/crates/solver-types/src/validation.rs @@ -56,7 +56,7 @@ pub enum FieldType { /// Validators are custom functions that can perform additional validation /// beyond type checking. They receive a TOML value and return an error /// message if validation fails. -pub type FieldValidator = Box Result<(), String> + Send + Sync>; +pub type FieldValidator = Box Result<(), String> + Send + Sync>; /// Represents a field in a configuration schema. /// @@ -104,7 +104,7 @@ impl Field { /// * `validator` - A closure that validates the field value pub fn with_validator(mut self, validator: F) -> Self where - F: Fn(&toml::Value) -> Result<(), String> + Send + Sync + 'static, + F: Fn(&serde_json::Value) -> Result<(), String> + Send + Sync + 'static, { self.validator = Some(Box::new(validator)); self @@ -159,13 +159,13 @@ impl Schema { /// - A field has the wrong type /// - A custom validator fails /// - A nested schema validation fails - pub fn validate(&self, config: &toml::Value) -> Result<(), ValidationError> { + pub fn validate(&self, config: &serde_json::Value) -> Result<(), ValidationError> { let table = config - .as_table() + .as_object() .ok_or_else(|| ValidationError::TypeMismatch { field: "root".to_string(), - expected: "table".to_string(), - actual: config.type_str().to_string(), + expected: "object".to_string(), + actual: json_type_name(config).to_string(), })?; // Check required fields @@ -222,26 +222,26 @@ impl Schema { /// * `Err(ValidationError)` with details about the type mismatch fn validate_field_type( field_name: &str, - value: &toml::Value, + value: &serde_json::Value, expected_type: &FieldType, ) -> Result<(), ValidationError> { match expected_type { FieldType::String => { - if !value.is_str() { + if value.as_str().is_none() { return Err(ValidationError::TypeMismatch { field: field_name.to_string(), expected: "string".to_string(), - actual: value.type_str().to_string(), + actual: json_type_name(value).to_string(), }); } }, FieldType::Integer { min, max } => { let int_val = value - .as_integer() + .as_i64() .ok_or_else(|| ValidationError::TypeMismatch { field: field_name.to_string(), expected: "integer".to_string(), - actual: value.type_str().to_string(), + actual: json_type_name(value).to_string(), })?; if let Some(min_val) = min { @@ -263,11 +263,11 @@ fn validate_field_type( } }, FieldType::Boolean => { - if !value.is_bool() { + if value.as_bool().is_none() { return Err(ValidationError::TypeMismatch { field: field_name.to_string(), expected: "boolean".to_string(), - actual: value.type_str().to_string(), + actual: json_type_name(value).to_string(), }); } }, @@ -277,7 +277,7 @@ fn validate_field_type( .ok_or_else(|| ValidationError::TypeMismatch { field: field_name.to_string(), expected: "array".to_string(), - actual: value.type_str().to_string(), + actual: json_type_name(value).to_string(), })?; for (i, item) in array.iter().enumerate() { @@ -310,26 +310,42 @@ fn validate_field_type( Ok(()) } -/// Trait defining a configuration schema that can validate TOML values. +fn json_type_name(value: &serde_json::Value) -> &'static str { + match value { + serde_json::Value::Null => "null", + serde_json::Value::Bool(_) => "boolean", + serde_json::Value::Number(n) if n.is_i64() || n.is_u64() => "integer", + serde_json::Value::Number(_) => "number", + serde_json::Value::String(_) => "string", + serde_json::Value::Array(_) => "array", + serde_json::Value::Object(_) => "object", + } +} + +/// Trait defining a configuration schema that can validate JSON values. /// /// Implement this trait to create custom configuration validators that can /// be used across different parts of the application. This is particularly /// useful for plugin systems or when you need polymorphic validation behavior. #[async_trait] pub trait ConfigSchema: Send + Sync { - /// Validates a TOML configuration value against this schema. + /// Validates a JSON configuration value against this schema. /// /// This method should check: /// - Required fields are present /// - Field types are correct /// - Values meet any constraints (ranges, patterns, etc.) - fn validate(&self, config: &toml::Value) -> Result<(), ValidationError>; + fn validate(&self, config: &serde_json::Value) -> Result<(), ValidationError>; } #[cfg(test)] mod tests { use super::*; - use toml::Value; + use serde_json::Value; + + fn parse_config(input: &str) -> Value { + serde_json::from_str(input).expect("test fixtures must be valid JSON") + } #[test] fn test_validation_error_display() { @@ -384,7 +400,7 @@ mod tests { }, ) .with_validator(|value| { - let port = value.as_integer().unwrap(); + let port = value.as_i64().unwrap(); if port > 0 && port <= 65535 { Ok(()) } else { @@ -421,10 +437,10 @@ mod tests { fn test_validate_string_field() { let schema = Schema::new(vec![Field::new("name", FieldType::String)], vec![]); - let valid_config = toml::from_str(r#"name = "test""#).unwrap(); + let valid_config = parse_config(r#"{"name":"test"}"#); assert!(schema.validate(&valid_config).is_ok()); - let invalid_config = toml::from_str(r#"name = 123"#).unwrap(); + let invalid_config = parse_config(r#"{"name":123}"#); let result = schema.validate(&invalid_config); assert!(result.is_err()); assert!(matches!( @@ -446,10 +462,10 @@ mod tests { vec![], ); - let valid_config = toml::from_str(r#"port = 8080"#).unwrap(); + let valid_config = parse_config(r#"{"port":8080}"#); assert!(schema.validate(&valid_config).is_ok()); - let too_small = toml::from_str(r#"port = 0"#).unwrap(); + let too_small = parse_config(r#"{"port":0}"#); let result = schema.validate(&too_small); assert!(result.is_err()); assert!(matches!( @@ -457,7 +473,7 @@ mod tests { ValidationError::InvalidValue { .. } )); - let too_large = toml::from_str(r#"port = 70000"#).unwrap(); + let too_large = parse_config(r#"{"port":70000}"#); let result = schema.validate(&too_large); assert!(result.is_err()); assert!(matches!( @@ -470,10 +486,10 @@ mod tests { fn test_validate_boolean_field() { let schema = Schema::new(vec![Field::new("enabled", FieldType::Boolean)], vec![]); - let valid_config = toml::from_str(r#"enabled = true"#).unwrap(); + let valid_config = parse_config(r#"{"enabled":true}"#); assert!(schema.validate(&valid_config).is_ok()); - let invalid_config = toml::from_str(r#"enabled = "yes""#).unwrap(); + let invalid_config = parse_config(r#"{"enabled":"yes"}"#); let result = schema.validate(&invalid_config); assert!(result.is_err()); assert!(matches!( @@ -492,10 +508,10 @@ mod tests { vec![], ); - let valid_config = toml::from_str(r#"tags = ["tag1", "tag2"]"#).unwrap(); + let valid_config = parse_config(r#"{"tags":["tag1","tag2"]}"#); assert!(schema.validate(&valid_config).is_ok()); - let invalid_config = toml::from_str(r#"tags = [1, 2, 3]"#).unwrap(); + let invalid_config = parse_config(r#"{"tags":[1,2,3]}"#); let result = schema.validate(&invalid_config); assert!(result.is_err()); assert!(matches!( @@ -522,23 +538,18 @@ mod tests { vec![], ); - let valid_config = toml::from_str( + let valid_config = parse_config( r#" - [database] - host = "localhost" - port = 5432 - "#, - ) - .unwrap(); + {"database":{"host":"localhost","port":5432}} + "#, + ); assert!(schema.validate(&valid_config).is_ok()); - let invalid_config = toml::from_str( + let invalid_config = parse_config( r#" - [database] - port = 5432 - "#, - ) - .unwrap(); + {"database":{"port":5432}} + "#, + ); let result = schema.validate(&invalid_config); assert!(result.is_err()); assert!(matches!( @@ -554,7 +565,7 @@ mod tests { vec![], ); - let config = toml::from_str(r#"other_field = "value""#).unwrap(); + let config = parse_config(r#"{"other_field":"value"}"#); let result = schema.validate(&config); assert!(result.is_err()); @@ -573,27 +584,23 @@ mod tests { ); // Config without optional field should be valid - let config_without_optional = toml::from_str(r#"name = "test""#).unwrap(); + let config_without_optional = parse_config(r#"{"name":"test"}"#); assert!(schema.validate(&config_without_optional).is_ok()); // Config with valid optional field should be valid - let config_with_optional = toml::from_str( + let config_with_optional = parse_config( r#" - name = "test" - enabled = true - "#, - ) - .unwrap(); + {"name":"test","enabled":true} + "#, + ); assert!(schema.validate(&config_with_optional).is_ok()); // Config with invalid optional field should fail - let config_with_invalid_optional = toml::from_str( + let config_with_invalid_optional = parse_config( r#" - name = "test" - enabled = "yes" - "#, - ) - .unwrap(); + {"name":"test","enabled":"yes"} + "#, + ); let result = schema.validate(&config_with_invalid_optional); assert!(result.is_err()); } @@ -608,7 +615,7 @@ mod tests { }, ) .with_validator(|value| { - let port = value.as_integer().unwrap(); + let port = value.as_i64().unwrap(); if port > 1024 { Ok(()) } else { @@ -617,7 +624,7 @@ mod tests { }); let schema = Schema::new(vec![field], vec![]); - let config = toml::from_str(r#"port = 8080"#).unwrap(); + let config = parse_config(r#"{"port":8080}"#); assert!(schema.validate(&config).is_ok()); } @@ -631,7 +638,7 @@ mod tests { }, ) .with_validator(|value| { - let port = value.as_integer().unwrap(); + let port = value.as_i64().unwrap(); if port > 1024 { Ok(()) } else { @@ -640,7 +647,7 @@ mod tests { }); let schema = Schema::new(vec![field], vec![]); - let config = toml::from_str(r#"port = 80"#).unwrap(); + let config = parse_config(r#"{"port":80}"#); let result = schema.validate(&config); assert!(result.is_err()); @@ -678,10 +685,10 @@ mod tests { ); // Valid values - let valid_configs = [r#"count = 0"#, r#"count = 50"#, r#"count = 100"#]; + let valid_configs = [r#"{"count":0}"#, r#"{"count":50}"#, r#"{"count":100}"#]; for config_str in &valid_configs { - let config = toml::from_str(config_str).unwrap(); + let config = parse_config(config_str); assert!( schema.validate(&config).is_ok(), "Failed for config: {config_str}" @@ -689,10 +696,10 @@ mod tests { } // Invalid values - let invalid_configs = [r#"count = -1"#, r#"count = 101"#]; + let invalid_configs = [r#"{"count":-1}"#, r#"{"count":101}"#]; for config_str in &invalid_configs { - let config = toml::from_str(config_str).unwrap(); + let config = parse_config(config_str); let result = schema.validate(&config); assert!( result.is_err(), @@ -718,10 +725,10 @@ mod tests { vec![], ); - let valid_config = toml::from_str(r#"numbers = [1, 2, 3]"#).unwrap(); + let valid_config = parse_config(r#"{"numbers":[1,2,3]}"#); assert!(schema.validate(&valid_config).is_ok()); - let invalid_config = toml::from_str(r#"numbers = [1, "two", 3]"#).unwrap(); + let invalid_config = parse_config(r#"{"numbers":[1,"two",3]}"#); let result = schema.validate(&invalid_config); assert!(result.is_err()); assert!(matches!( @@ -740,7 +747,7 @@ mod tests { vec![], ); - let config = toml::from_str(r#"items = []"#).unwrap(); + let config = parse_config(r#"{"items":[]}"#); assert!(schema.validate(&config).is_ok()); } @@ -756,13 +763,11 @@ mod tests { vec![], ); - let invalid_config = toml::from_str( + let invalid_config = parse_config( r#" - [config] - other_field = "value" - "#, - ) - .unwrap(); + {"config":{"other_field":"value"}} + "#, + ); let result = schema.validate(&invalid_config); assert!(result.is_err()); @@ -804,17 +809,11 @@ mod tests { vec![], ); - let valid_config = toml::from_str( + let valid_config = parse_config( r#" - app_name = "my_app" - - [database] - host = "localhost" - port = 5432 - timeout = 30 - "#, - ) - .unwrap(); + {"app_name":"my_app","database":{"host":"localhost","port":5432,"timeout":30}} + "#, + ); assert!(schema.validate(&valid_config).is_ok()); } @@ -833,7 +832,7 @@ mod tests { #[async_trait] impl ConfigSchema for TestConfigSchema { - fn validate(&self, config: &toml::Value) -> Result<(), ValidationError> { + fn validate(&self, config: &serde_json::Value) -> Result<(), ValidationError> { self.schema.validate(config) } } @@ -842,10 +841,10 @@ mod tests { fn test_config_schema_trait() { let config_schema = TestConfigSchema::new(); - let valid_config = toml::from_str(r#"test_field = "value""#).unwrap(); + let valid_config = parse_config(r#"{"test_field":"value"}"#); assert!(config_schema.validate(&valid_config).is_ok()); - let invalid_config = toml::from_str(r#"other_field = "value""#).unwrap(); + let invalid_config = parse_config(r#"{"other_field":"value"}"#); let result = config_schema.validate(&invalid_config); assert!(result.is_err()); assert!(matches!( @@ -867,10 +866,14 @@ mod tests { vec![], ); - let configs = [r#"value = -1000"#, r#"value = 0"#, r#"value = 1000000"#]; + let configs = [ + r#"{"value":-1000}"#, + r#"{"value":0}"#, + r#"{"value":1000000}"#, + ]; for config_str in &configs { - let config = toml::from_str(config_str).unwrap(); + let config = parse_config(config_str); assert!(schema.validate(&config).is_ok(), "Failed for: {config_str}"); } } @@ -890,17 +893,11 @@ mod tests { vec![], ); - let valid_config = toml::from_str( + let valid_config = parse_config( r#" - [[items]] - name = "item1" - enabled = true - - [[items]] - name = "item2" - "#, - ) - .unwrap(); + {"items":[{"name":"item1","enabled":true},{"name":"item2"}]} + "#, + ); assert!(schema.validate(&valid_config).is_ok()); } @@ -914,13 +911,11 @@ mod tests { vec![], ); - let config = toml::from_str( + let config = parse_config( r#" - [outer] - inner = 123 - "#, - ) - .unwrap(); + {"outer":{"inner":123}} + "#, + ); let result = schema.validate(&config); assert!(result.is_err()); diff --git a/scripts/e2e/batch_intents.sh b/scripts/e2e/batch_intents.sh index e8894f87..3b49707f 100755 --- a/scripts/e2e/batch_intents.sh +++ b/scripts/e2e/batch_intents.sh @@ -94,14 +94,13 @@ if ! jq empty "$INTENTS_FILE" 2>/dev/null; then exit 1 fi -if [ ! -f "config/testnet.toml" ] || [ ! -f "config/testnet/networks.toml" ]; then +if [ ! -f "config/testnet.json" ]; then echo -e "${RED}❌ Testnet configuration not found!${NC}" exit 1 fi # Configuration -MAIN_CONFIG="config/testnet.toml" -NETWORKS_CONFIG="config/testnet/networks.toml" +MAIN_CONFIG="config/testnet.json" TESTNET_CONFIG="scripts/e2e/testnet-config.json" # Load environment variables from .env file if it exists @@ -147,13 +146,13 @@ get_network_config() { case $config_type in "input_settler") - grep -A 5 "\[networks\.${chain_id}\]" $NETWORKS_CONFIG | grep 'input_settler_address = ' | cut -d'"' -f2 + jq -r --arg chain_id "$chain_id" '.networks[$chain_id].input_settler_address // empty' "$MAIN_CONFIG" ;; "output_settler") - grep -A 5 "\[networks\.${chain_id}\]" $NETWORKS_CONFIG | grep 'output_settler_address = ' | cut -d'"' -f2 + jq -r --arg chain_id "$chain_id" '.networks[$chain_id].output_settler_address // empty' "$MAIN_CONFIG" ;; "rpc_url") - awk "/\[\[networks\.${chain_id}\.rpc_urls\]\]/{f=1} f && /^http = /{print; exit}" $NETWORKS_CONFIG | cut -d'"' -f2 + jq -r --arg chain_id "$chain_id" '.networks[$chain_id].rpc_urls[0].http // empty' "$MAIN_CONFIG" ;; "oracle") # Get oracle from any settlement configuration that supports this chain @@ -471,4 +470,4 @@ process_batch_sequential() { # Execute main function process_batch_sequential -echo -e "${GREEN}🎉 All intents processed!${NC}" \ No newline at end of file +echo -e "${GREEN}🎉 All intents processed!${NC}" diff --git a/scripts/e2e/setup_testnet.sh b/scripts/e2e/setup_testnet.sh index 4da1af01..a94cd242 100755 --- a/scripts/e2e/setup_testnet.sh +++ b/scripts/e2e/setup_testnet.sh @@ -183,25 +183,23 @@ generate_solver_configs() { echo -e "${YELLOW}Generating solver configuration files...${NC}" if [ "$DRY_RUN" = true ]; then - echo " [DRY RUN] Would create config/testnet.toml" - echo " [DRY RUN] Would create config/testnet/networks.toml" - echo " [DRY RUN] Would create config/testnet/api.toml" - echo " [DRY RUN] Would create config/testnet/gas.toml" + echo " [DRY RUN] Would create config/testnet.json" + echo " [DRY RUN] Would create config/testnet/networks.json" + echo " [DRY RUN] Would create config/testnet/api.json" + echo " [DRY RUN] Would create config/testnet/gas.json" return 0 fi mkdir -p "$PROJECT_ROOT/config/testnet" - # Generate main config file - generate_main_config "$origin_id" "$dest_id" - - # Generate networks config + # Generate base config and then append enabled settlement implementations. + generate_main_config "$origin_id" "$dest_id" "$origin_rpc" "$dest_rpc" + append_enabled_settlements "$origin_id" "$dest_id" + finalize_order_implementations + + # Generate section snapshots for convenience. generate_networks_config "$origin_id" "$dest_id" "$origin_rpc" "$dest_rpc" - - # Generate API config generate_api_config - - # Generate gas config generate_gas_config echo -e "${GREEN}✅ Configuration files generated${NC}" @@ -211,210 +209,246 @@ generate_solver_configs() { generate_main_config() { local origin_id="$1" local dest_id="$2" - - cat > "$PROJECT_ROOT/config/testnet.toml" << EOF -# OIF Solver Configuration - Generated by setup_testnet.sh -# Origin Chain ID: $origin_id -# Destination Chain ID: $dest_id - -include = [ - "testnet/networks.toml", - "testnet/api.toml", - "testnet/gas.toml" -] - -[solver] -id = "$(get_config ".solver_parameters.id" "oif-solver-testnet")" -monitoring_timeout_minutes = $(get_config ".solver_parameters.monitoring_timeout_minutes" "5") -min_profitability_pct = $(get_config ".solver_parameters.min_profitability_pct" "1.0") - -# ============================================================================ -# STORAGE -# ============================================================================ -[storage] -primary = "$(get_config ".storage.primary" "file")" -cleanup_interval_seconds = $(get_config ".solver_parameters.cleanup_interval_seconds" "3600") - -[storage.implementations.memory] -# Memory storage has no configuration - -[storage.implementations.file] -storage_path = "$(get_config ".storage.file.storage_path" "./data/storage")" -ttl_orders = $(get_config ".storage.file.ttl_orders" "0") -ttl_intents = $(get_config ".storage.file.ttl_intents" "86400") -ttl_order_by_tx_hash = $(get_config ".storage.file.ttl_order_by_tx_hash" "86400") - -# ============================================================================ -# ACCOUNT -# ============================================================================ -[account] -primary = "local" - -[account.implementations.local] -private_key = "\${SOLVER_PRIVATE_KEY}" - -# ============================================================================ -# DELIVERY -# ============================================================================ -[delivery] -min_confirmations = $(get_config ".solver_parameters.min_confirmations" "3") -transaction_poll_interval_seconds = 3 - -[delivery.implementations.evm_alloy] -network_ids = [$origin_id, $dest_id] - -# ============================================================================ -# DISCOVERY -# ============================================================================ -[discovery] - -[discovery.implementations.onchain_eip7683] -network_ids = [$origin_id, $dest_id] -polling_interval_secs = $(get_config ".discovery.onchain.polling_interval_secs" "0") - -[discovery.implementations.offchain_eip7683] -api_host = "$(get_config ".api.discovery_api.host" "127.0.0.1")" -api_port = $(get_config ".api.discovery_api.port" "8081") -network_ids = [$origin_id, $dest_id] - -# ============================================================================ -# ORDER -# ============================================================================ -[order] - -[order.implementations.eip7683] - -[order.strategy] -primary = "simple" - -[order.strategy.implementations.simple] -max_gas_price_gwei = $(get_config ".solver_parameters.max_gas_price_gwei" "100") - -# ============================================================================ -# PRICING -# ============================================================================ -[pricing] -primary = "$(get_config ".pricing.primary" "coingecko")" - -[pricing.implementations.coingecko] -cache_duration_seconds = $(get_config ".pricing.coingecko.cache_duration_seconds" "60") -rate_limit_delay_ms = $(get_config ".pricing.coingecko.rate_limit_delay_ms" "1200") - -# ============================================================================ -# SETTLEMENT -# ============================================================================ -[settlement] -settlement_poll_interval_seconds = 3 - -[settlement.domain] -chain_id = $origin_id -address = "${INPUT_SETTLER_ADDRESS_ORIGIN:-0x0000000000000000000000000000000000000000}" - -[settlement.implementations.direct] -order = "$(get_config ".settlement.direct.order_type" "eipXXXX")" -network_ids = [$origin_id, $dest_id] -dispute_period_seconds = $(get_config ".settlement.direct.dispute_period_seconds" "60") -oracle_selection_strategy = "$(get_config ".settlement.direct.oracle_selection_strategy" "First")" - -[settlement.implementations.direct.oracles] -input = { $origin_id = ["${ORACLE_ADDRESS_ORIGIN:-0x0}"], $dest_id = ["${ORACLE_ADDRESS_DEST:-0x0}"] } -output = { $origin_id = ["${ORACLE_ADDRESS_ORIGIN:-0x0}"], $dest_id = ["${ORACLE_ADDRESS_DEST:-0x0}"] } - -[settlement.implementations.direct.routes] -# Bidirectional routes - both chains can send to each other -$origin_id = [$dest_id] -$dest_id = [$origin_id] -EOF - - # Add configurations for all enabled settlement methods - append_enabled_settlements "$origin_id" "$dest_id" + local origin_rpc="$3" + local dest_rpc="$4" + local solver_id=$(get_config ".solver_parameters.id" "oif-solver-testnet") + local monitoring_timeout_minutes=$(get_config ".solver_parameters.monitoring_timeout_minutes" "5") + local monitoring_timeout_seconds=$((monitoring_timeout_minutes * 60)) + local min_profitability_pct=$(get_config ".solver_parameters.min_profitability_pct" "1.0") + local cleanup_interval_seconds=$(get_config ".solver_parameters.cleanup_interval_seconds" "3600") + local min_confirmations=$(get_config ".solver_parameters.min_confirmations" "3") + local max_gas_price_gwei=$(get_config ".solver_parameters.max_gas_price_gwei" "100") + + local storage_primary=$(get_config ".storage.primary" "file") + local storage_path=$(get_config ".storage.file.storage_path" "./data/storage") + local ttl_orders=$(get_config ".storage.file.ttl_orders" "0") + local ttl_intents=$(get_config ".storage.file.ttl_intents" "86400") + local ttl_order_by_tx_hash=$(get_config ".storage.file.ttl_order_by_tx_hash" "86400") + + local onchain_polling=$(get_config ".discovery.onchain.polling_interval_secs" "0") + local discovery_api_host=$(get_config ".api.discovery_api.host" "127.0.0.1") + local discovery_api_port=$(get_config ".api.discovery_api.port" "8081") + + local pricing_primary=$(get_config ".pricing.primary" "coingecko") + local coingecko_cache=$(get_config ".pricing.coingecko.cache_duration_seconds" "60") + local coingecko_delay=$(get_config ".pricing.coingecko.rate_limit_delay_ms" "1200") + local coingecko_api_key_env=$(get_config ".pricing.coingecko.api_key_env" "COINGECKO_API_KEY") + local coingecko_api_key_placeholder="\${${coingecko_api_key_env}:-}" + + local api_enabled=$(get_config ".api.solver_api.enabled" "true") + local api_host=$(get_config ".api.solver_api.host" "127.0.0.1") + local api_port=$(get_config ".api.solver_api.port" "3000") + local api_timeout=$(get_config ".api.solver_api.timeout_seconds" "30") + local api_max_request_size=$(get_config ".api.solver_api.max_request_size" "1048576") + local auth_enabled=$(get_config ".api.auth.enabled" "false") + local auth_access_expiry=$(get_config ".api.auth.access_token_expiry_hours" "1") + local auth_refresh_expiry=$(get_config ".api.auth.refresh_token_expiry_hours" "720") + local auth_issuer=$(get_config ".api.auth.issuer" "oif-solver-testnet") + local auth_secret_env=$(get_config ".api.auth.jwt_secret_env" "JWT_SECRET") + local auth_jwt_secret_placeholder="\${${auth_secret_env}:-DefaultSecret123}" + local quote_validity=$(get_config ".api.quote.validity_seconds" "60") + + local origin_input_settler="${INPUT_SETTLER_ADDRESS_ORIGIN:-0x0000000000000000000000000000000000000000}" + local origin_output_settler="${OUTPUT_SETTLER_ADDRESS_ORIGIN:-0x0000000000000000000000000000000000000000}" + local dest_input_settler="${INPUT_SETTLER_ADDRESS_DEST:-0x0000000000000000000000000000000000000000}" + local dest_output_settler="${OUTPUT_SETTLER_ADDRESS_DEST:-0x0000000000000000000000000000000000000000}" + local origin_token_address=$(get_chain_data "$ORIGIN_CHAIN" "usdc_address") + local dest_token_address=$(get_chain_data "$DEST_CHAIN" "usdc_address") + + jq -n \ + --arg solver_id "$solver_id" \ + --arg min_profitability_pct "$min_profitability_pct" \ + --argjson monitoring_timeout_seconds "$monitoring_timeout_seconds" \ + --arg storage_primary "$storage_primary" \ + --arg storage_path "$storage_path" \ + --argjson cleanup_interval_seconds "$cleanup_interval_seconds" \ + --argjson ttl_orders "$ttl_orders" \ + --argjson ttl_intents "$ttl_intents" \ + --argjson ttl_order_by_tx_hash "$ttl_order_by_tx_hash" \ + --argjson min_confirmations "$min_confirmations" \ + --argjson origin_id "$origin_id" \ + --argjson dest_id "$dest_id" \ + --arg origin_id_key "$origin_id" \ + --arg dest_id_key "$dest_id" \ + --arg origin_rpc "$origin_rpc" \ + --arg dest_rpc "$dest_rpc" \ + --arg origin_input_settler "$origin_input_settler" \ + --arg origin_output_settler "$origin_output_settler" \ + --arg dest_input_settler "$dest_input_settler" \ + --arg dest_output_settler "$dest_output_settler" \ + --arg origin_token_address "$origin_token_address" \ + --arg dest_token_address "$dest_token_address" \ + --argjson onchain_polling "$onchain_polling" \ + --arg discovery_api_host "$discovery_api_host" \ + --argjson discovery_api_port "$discovery_api_port" \ + --argjson max_gas_price_gwei "$max_gas_price_gwei" \ + --arg pricing_primary "$pricing_primary" \ + --argjson coingecko_cache "$coingecko_cache" \ + --argjson coingecko_delay "$coingecko_delay" \ + --arg coingecko_api_key "$coingecko_api_key_placeholder" \ + --argjson api_enabled "$api_enabled" \ + --arg api_host "$api_host" \ + --argjson api_port "$api_port" \ + --argjson api_timeout "$api_timeout" \ + --argjson api_max_request_size "$api_max_request_size" \ + --argjson auth_enabled "$auth_enabled" \ + --arg auth_jwt_secret "$auth_jwt_secret_placeholder" \ + --argjson auth_access_expiry "$auth_access_expiry" \ + --argjson auth_refresh_expiry "$auth_refresh_expiry" \ + --arg auth_issuer "$auth_issuer" \ + --argjson quote_validity "$quote_validity" \ + --arg solver_private_key '${SOLVER_PRIVATE_KEY}' \ + --argjson gas_resource_lock_open "$(get_config ".gas_estimates.resource_lock.open" "0")" \ + --argjson gas_resource_lock_fill "$(get_config ".gas_estimates.resource_lock.fill" "77298")" \ + --argjson gas_resource_lock_claim "$(get_config ".gas_estimates.resource_lock.claim" "122793")" \ + --argjson gas_permit2_open "$(get_config ".gas_estimates.permit2_escrow.open" "146306")" \ + --argjson gas_permit2_fill "$(get_config ".gas_estimates.permit2_escrow.fill" "77298")" \ + --argjson gas_permit2_claim "$(get_config ".gas_estimates.permit2_escrow.claim" "60084")" \ + --argjson gas_eip3009_open "$(get_config ".gas_estimates.eip3009_escrow.open" "130254")" \ + --argjson gas_eip3009_fill "$(get_config ".gas_estimates.eip3009_escrow.fill" "77298")" \ + --argjson gas_eip3009_claim "$(get_config ".gas_estimates.eip3009_escrow.claim" "60084")" \ + '{ + solver: { + id: $solver_id, + min_profitability_pct: $min_profitability_pct, + monitoring_timeout_seconds: $monitoring_timeout_seconds + }, + networks: { + ($origin_id_key): { + input_settler_address: $origin_input_settler, + output_settler_address: $origin_output_settler, + rpc_urls: [{http: $origin_rpc}], + tokens: [{address: $origin_token_address, symbol: "USDC", decimals: 6}] + }, + ($dest_id_key): { + input_settler_address: $dest_input_settler, + output_settler_address: $dest_output_settler, + rpc_urls: [{http: $dest_rpc}], + tokens: [{address: $dest_token_address, symbol: "USDC", decimals: 6}] + } + }, + storage: { + primary: $storage_primary, + cleanup_interval_seconds: $cleanup_interval_seconds, + implementations: { + memory: {}, + file: { + storage_path: $storage_path, + ttl_orders: $ttl_orders, + ttl_intents: $ttl_intents, + ttl_order_by_tx_hash: $ttl_order_by_tx_hash + } + } + }, + delivery: { + min_confirmations: $min_confirmations, + implementations: { + evm_alloy: { + network_ids: [$origin_id, $dest_id] + } + } + }, + account: { + primary: "local", + implementations: { + local: { + private_key: $solver_private_key + } + } + }, + discovery: { + implementations: { + onchain_eip7683: { + network_ids: [$origin_id, $dest_id], + polling_interval_secs: $onchain_polling + }, + offchain_eip7683: { + api_host: $discovery_api_host, + api_port: $discovery_api_port, + network_ids: [$origin_id, $dest_id] + } + } + }, + order: { + implementations: {}, + strategy: { + primary: "simple", + implementations: { + simple: { + max_gas_price_gwei: $max_gas_price_gwei + } + } + } + }, + settlement: { + settlement_poll_interval_seconds: 3, + implementations: {} + }, + pricing: { + primary: $pricing_primary, + implementations: { + coingecko: { + cache_duration_seconds: $coingecko_cache, + rate_limit_delay_ms: $coingecko_delay, + api_key: $coingecko_api_key + } + } + }, + api: { + enabled: $api_enabled, + host: $api_host, + port: $api_port, + timeout_seconds: $api_timeout, + max_request_size: $api_max_request_size, + implementations: { + discovery: "offchain_eip7683" + }, + auth: { + enabled: $auth_enabled, + jwt_secret: $auth_jwt_secret, + access_token_expiry_hours: $auth_access_expiry, + refresh_token_expiry_hours: $auth_refresh_expiry, + issuer: $auth_issuer + }, + quote: { + validity_seconds: $quote_validity + } + }, + gas: { + flows: { + resource_lock: { + open: $gas_resource_lock_open, + fill: $gas_resource_lock_fill, + claim: $gas_resource_lock_claim + }, + permit2_escrow: { + open: $gas_permit2_open, + fill: $gas_permit2_fill, + claim: $gas_permit2_claim + }, + eip3009_escrow: { + open: $gas_eip3009_open, + fill: $gas_eip3009_fill, + claim: $gas_eip3009_claim + } + } + } + }' > "$PROJECT_ROOT/config/testnet.json" } # Generate networks configuration generate_networks_config() { - local origin_id="$1" - local dest_id="$2" - local origin_rpc="$3" - local dest_rpc="$4" - - cat > "$PROJECT_ROOT/config/testnet/networks.toml" << EOF -# Network Configuration - Generated by setup_testnet.sh - -[networks.$origin_id] -input_settler_address = "${INPUT_SETTLER_ADDRESS_ORIGIN:-0x0000000000000000000000000000000000000000}" -output_settler_address = "${OUTPUT_SETTLER_ADDRESS_ORIGIN:-0x0000000000000000000000000000000000000000}" - -[[networks.$origin_id.rpc_urls]] -http = "$origin_rpc" - -[[networks.$origin_id.tokens]] -address = "$(get_chain_data "$ORIGIN_CHAIN" "usdc_address")" -symbol = "USDC" -decimals = 6 - -[networks.$dest_id] -input_settler_address = "${INPUT_SETTLER_ADDRESS_DEST:-0x0000000000000000000000000000000000000000}" -output_settler_address = "${OUTPUT_SETTLER_ADDRESS_DEST:-0x0000000000000000000000000000000000000000}" - -[[networks.$dest_id.rpc_urls]] -http = "$dest_rpc" - -[[networks.$dest_id.tokens]] -address = "$(get_chain_data "$DEST_CHAIN" "usdc_address")" -symbol = "USDC" -decimals = 6 -EOF + jq '{networks: .networks}' "$PROJECT_ROOT/config/testnet.json" > "$PROJECT_ROOT/config/testnet/networks.json" } # Generate API configuration generate_api_config() { - cat > "$PROJECT_ROOT/config/testnet/api.toml" << EOF -# API Configuration - Generated by setup_testnet.sh - -[api] -enabled = $(get_config ".api.solver_api.enabled" "true") -host = "$(get_config ".api.solver_api.host" "127.0.0.1")" -port = $(get_config ".api.solver_api.port" "3000") -timeout_seconds = $(get_config ".api.solver_api.timeout_seconds" "30") -max_request_size = $(get_config ".api.solver_api.max_request_size" "1048576") - -[api.implementations] -discovery = "offchain_eip7683" - -[api.auth] -enabled = $(get_config ".api.auth.enabled" "false") -jwt_secret = "\${JWT_SECRET:-$(get_config ".api.auth.jwt_secret_env" "DefaultSecret123")}" -access_token_expiry_hours = $(get_config ".api.auth.access_token_expiry_hours" "1") -refresh_token_expiry_hours = $(get_config ".api.auth.refresh_token_expiry_hours" "720") -issuer = "$(get_config ".api.auth.issuer" "oif-solver-testnet")" - -[api.quote] -validity_seconds = $(get_config ".api.quote.validity_seconds" "60") -EOF + jq '{api: .api}' "$PROJECT_ROOT/config/testnet.json" > "$PROJECT_ROOT/config/testnet/api.json" } # Generate gas configuration generate_gas_config() { - cat > "$PROJECT_ROOT/config/testnet/gas.toml" << EOF -# Gas Configuration - Generated by setup_testnet.sh - -[gas] - -[gas.flows.resource_lock] -open = $(get_config ".gas_estimates.resource_lock.open" "0") -fill = $(get_config ".gas_estimates.resource_lock.fill" "77298") -claim = $(get_config ".gas_estimates.resource_lock.claim" "122793") - -[gas.flows.permit2_escrow] -open = $(get_config ".gas_estimates.permit2_escrow.open" "146306") -fill = $(get_config ".gas_estimates.permit2_escrow.fill" "77298") -claim = $(get_config ".gas_estimates.permit2_escrow.claim" "60084") - -[gas.flows.eip3009_escrow] -open = $(get_config ".gas_estimates.eip3009_escrow.open" "130254") -fill = $(get_config ".gas_estimates.eip3009_escrow.fill" "77298") -claim = $(get_config ".gas_estimates.eip3009_escrow.claim" "60084") -EOF + jq '{gas: .gas}' "$PROJECT_ROOT/config/testnet.json" > "$PROJECT_ROOT/config/testnet/gas.json" } # Append all enabled settlement configurations @@ -482,6 +516,26 @@ is_route_supported() { return 1 } +finalize_order_implementations() { + local impl_count + impl_count=$(jq -r '.settlement.implementations | length' "$PROJECT_ROOT/config/testnet.json") + + if [ "$impl_count" -eq 0 ]; then + echo -e "${RED}❌ No settlement implementations enabled for route ${ORIGIN_CHAIN} -> ${DEST_CHAIN}${NC}" + exit 1 + fi + + local temp_file + temp_file=$(mktemp) + jq ' + .order.implementations = ( + .settlement.implementations + | to_entries + | reduce .[] as $impl ({}; .[$impl.value.order] = {}) + ) + ' "$PROJECT_ROOT/config/testnet.json" > "$temp_file" + mv "$temp_file" "$PROJECT_ROOT/config/testnet.json" +} # Append configuration for a specific settlement method append_settlement_config() { @@ -496,8 +550,7 @@ append_settlement_config() { append_hyperlane_settlement "$impl_index" "$origin_id" "$dest_id" ;; "direct") - # Direct settlement is already added in the base config - echo " Direct settlement already configured" + append_direct_settlement "$impl_index" "$origin_id" "$dest_id" ;; *) echo " Warning: Unknown settlement type '$type', skipping..." @@ -505,67 +558,128 @@ append_settlement_config() { esac } +append_direct_settlement() { + local impl_index="$1" + local origin_id="$2" + local dest_id="$3" + local order_type=$(get_config ".settlement.implementations[$impl_index].order_type" "eipXXXX") + local dispute_period_seconds=$(get_config ".settlement.implementations[$impl_index].parameters.dispute_period_seconds" "60") + local oracle_selection_strategy=$(get_config ".settlement.implementations[$impl_index].parameters.oracle_selection_strategy" "First") + + local origin_oracle_from_config + local dest_oracle_from_config + origin_oracle_from_config=$(jq -r --arg origin_chain "$ORIGIN_CHAIN" ".settlement.implementations[$impl_index].chains[\$origin_chain].oracle // \"0x0000000000000000000000000000000000000000\"" "$CONFIG_FILE") + dest_oracle_from_config=$(jq -r --arg dest_chain "$DEST_CHAIN" ".settlement.implementations[$impl_index].chains[\$dest_chain].oracle // \"0x0000000000000000000000000000000000000000\"" "$CONFIG_FILE") + + local origin_oracle="${ORACLE_ADDRESS_ORIGIN:-$origin_oracle_from_config}" + local dest_oracle="${ORACLE_ADDRESS_DEST:-$dest_oracle_from_config}" + + local temp_file + temp_file=$(mktemp) + jq \ + --arg order_type "$order_type" \ + --argjson origin_id "$origin_id" \ + --argjson dest_id "$dest_id" \ + --arg origin_oracle "$origin_oracle" \ + --arg dest_oracle "$dest_oracle" \ + --arg oracle_selection_strategy "$oracle_selection_strategy" \ + --argjson dispute_period_seconds "$dispute_period_seconds" \ + ' + .settlement.implementations.direct = { + order: $order_type, + network_ids: [$origin_id, $dest_id], + dispute_period_seconds: $dispute_period_seconds, + oracle_selection_strategy: $oracle_selection_strategy, + oracles: { + input: { + ($origin_id | tostring): [$origin_oracle], + ($dest_id | tostring): [$dest_oracle] + }, + output: { + ($origin_id | tostring): [$origin_oracle], + ($dest_id | tostring): [$dest_oracle] + } + }, + routes: { + ($origin_id | tostring): [$dest_id], + ($dest_id | tostring): [$origin_id] + } + } + ' "$PROJECT_ROOT/config/testnet.json" > "$temp_file" + mv "$temp_file" "$PROJECT_ROOT/config/testnet.json" +} + # Append Hyperlane settlement configuration append_hyperlane_settlement() { local impl_index="$1" local origin_id="$2" local dest_id="$3" - - # Get oracle addresses from the generalized config - local origin_oracle=$(jq -r --arg chain_id "$origin_id" \ - ".settlement.implementations[$impl_index].chains | to_entries[] | select(.value.chain_id == (\$chain_id | tonumber)) | .value.oracle" \ - "$CONFIG_FILE") - - local dest_oracle=$(jq -r --arg chain_id "$dest_id" \ - ".settlement.implementations[$impl_index].chains | to_entries[] | select(.value.chain_id == (\$chain_id | tonumber)) | .value.oracle" \ - "$CONFIG_FILE") - - # Get Hyperlane-specific config - local origin_mailbox=$(jq -r --arg chain_id "$origin_id" \ - ".settlement.implementations[$impl_index].chains | to_entries[] | select(.value.chain_id == (\$chain_id | tonumber)) | .value.mailbox" \ - "$CONFIG_FILE") - - local dest_mailbox=$(jq -r --arg chain_id "$dest_id" \ - ".settlement.implementations[$impl_index].chains | to_entries[] | select(.value.chain_id == (\$chain_id | tonumber)) | .value.mailbox" \ - "$CONFIG_FILE") - - local origin_igp=$(jq -r --arg chain_id "$origin_id" \ - ".settlement.implementations[$impl_index].chains | to_entries[] | select(.value.chain_id == (\$chain_id | tonumber)) | .value.igp" \ - "$CONFIG_FILE") - - local dest_igp=$(jq -r --arg chain_id "$dest_id" \ - ".settlement.implementations[$impl_index].chains | to_entries[] | select(.value.chain_id == (\$chain_id | tonumber)) | .value.igp" \ - "$CONFIG_FILE") - - cat >> "$PROJECT_ROOT/config/testnet.toml" << EOF - -# ============================================================================ -# HYPERLANE SETTLEMENT -# ============================================================================ -[settlement.implementations.hyperlane] -order = "$(get_config ".settlement.implementations[$impl_index].order_type" "eip7683")" -network_ids = [$origin_id, $dest_id] -default_gas_limit = $(get_config ".settlement.implementations[$impl_index].parameters.default_gas_limit" "500000") -message_timeout_seconds = $(get_config ".settlement.implementations[$impl_index].parameters.message_timeout_seconds" "600") -finalization_required = $(get_config ".settlement.implementations[$impl_index].parameters.finalization_required" "true") - -[settlement.implementations.hyperlane.oracles] -input = { $origin_id = ["$origin_oracle"], $dest_id = ["$dest_oracle"] } -output = { $origin_id = ["$origin_oracle"], $dest_id = ["$dest_oracle"] } - -[settlement.implementations.hyperlane.routes] -# Bidirectional routes - both chains can send to each other -$origin_id = [$dest_id] -$dest_id = [$origin_id] - -[settlement.implementations.hyperlane.mailboxes] -$origin_id = "$origin_mailbox" -$dest_id = "$dest_mailbox" - -[settlement.implementations.hyperlane.igp_addresses] -$origin_id = "$origin_igp" -$dest_id = "$dest_igp" -EOF + + local order_type=$(get_config ".settlement.implementations[$impl_index].order_type" "eip7683") + local default_gas_limit=$(get_config ".settlement.implementations[$impl_index].parameters.default_gas_limit" "500000") + local message_timeout_seconds=$(get_config ".settlement.implementations[$impl_index].parameters.message_timeout_seconds" "600") + local finalization_required=$(get_config ".settlement.implementations[$impl_index].parameters.finalization_required" "true") + + local origin_oracle + local dest_oracle + local origin_mailbox + local dest_mailbox + local origin_igp + local dest_igp + origin_oracle=$(jq -r --arg origin_chain "$ORIGIN_CHAIN" ".settlement.implementations[$impl_index].chains[\$origin_chain].oracle // \"0x0000000000000000000000000000000000000000\"" "$CONFIG_FILE") + dest_oracle=$(jq -r --arg dest_chain "$DEST_CHAIN" ".settlement.implementations[$impl_index].chains[\$dest_chain].oracle // \"0x0000000000000000000000000000000000000000\"" "$CONFIG_FILE") + origin_mailbox=$(jq -r --arg origin_chain "$ORIGIN_CHAIN" ".settlement.implementations[$impl_index].chains[\$origin_chain].mailbox // \"0x0000000000000000000000000000000000000000\"" "$CONFIG_FILE") + dest_mailbox=$(jq -r --arg dest_chain "$DEST_CHAIN" ".settlement.implementations[$impl_index].chains[\$dest_chain].mailbox // \"0x0000000000000000000000000000000000000000\"" "$CONFIG_FILE") + origin_igp=$(jq -r --arg origin_chain "$ORIGIN_CHAIN" ".settlement.implementations[$impl_index].chains[\$origin_chain].igp // \"0x0000000000000000000000000000000000000000\"" "$CONFIG_FILE") + dest_igp=$(jq -r --arg dest_chain "$DEST_CHAIN" ".settlement.implementations[$impl_index].chains[\$dest_chain].igp // \"0x0000000000000000000000000000000000000000\"" "$CONFIG_FILE") + + local temp_file + temp_file=$(mktemp) + jq \ + --arg order_type "$order_type" \ + --argjson origin_id "$origin_id" \ + --argjson dest_id "$dest_id" \ + --arg origin_oracle "$origin_oracle" \ + --arg dest_oracle "$dest_oracle" \ + --arg origin_mailbox "$origin_mailbox" \ + --arg dest_mailbox "$dest_mailbox" \ + --arg origin_igp "$origin_igp" \ + --arg dest_igp "$dest_igp" \ + --argjson default_gas_limit "$default_gas_limit" \ + --argjson message_timeout_seconds "$message_timeout_seconds" \ + --argjson finalization_required "$finalization_required" \ + ' + .settlement.implementations.hyperlane = { + order: $order_type, + network_ids: [$origin_id, $dest_id], + default_gas_limit: $default_gas_limit, + message_timeout_seconds: $message_timeout_seconds, + finalization_required: $finalization_required, + oracles: { + input: { + ($origin_id | tostring): [$origin_oracle], + ($dest_id | tostring): [$dest_oracle] + }, + output: { + ($origin_id | tostring): [$origin_oracle], + ($dest_id | tostring): [$dest_oracle] + } + }, + routes: { + ($origin_id | tostring): [$dest_id], + ($dest_id | tostring): [$origin_id] + }, + mailboxes: { + ($origin_id | tostring): $origin_mailbox, + ($dest_id | tostring): $dest_mailbox + }, + igp_addresses: { + ($origin_id | tostring): $origin_igp, + ($dest_id | tostring): $dest_igp + } + } + ' "$PROJECT_ROOT/config/testnet.json" > "$temp_file" + mv "$temp_file" "$PROJECT_ROOT/config/testnet.json" } # Show summary @@ -599,17 +713,17 @@ show_summary() { fi echo -e "${BLUE}📋 Configuration Files:${NC}" - echo " Main: config/testnet.toml" - echo " Networks: config/testnet/networks.toml" - echo " API: config/testnet/api.toml" - echo " Gas: config/testnet/gas.toml" + echo " Main: config/testnet.json" + echo " Networks: config/testnet/networks.json" + echo " API: config/testnet/api.json" + echo " Gas: config/testnet/gas.json" echo echo -e "${YELLOW}To start the solver:${NC}" echo " 1. Ensure environment variables are loaded:" echo " source .env" echo " 2. Run the solver:" - echo " cargo run --bin solver -- --config config/testnet.toml" + echo " cargo run --bin solver -- --config config/testnet.json" echo # Show enabled settlement methods @@ -722,4 +836,4 @@ main() { } # Run main function -main "$@" \ No newline at end of file +main "$@"