diff --git a/Cargo.lock b/Cargo.lock index 4b0ee3e..1f587dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3002,6 +3002,38 @@ dependencies = [ "uuid", ] +[[package]] +name = "engine-integration-tests" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.22.1", + "bincode 2.0.1", + "config", + "engine-core", + "engine-executors", + "engine-solana-core", + "moka", + "once_cell", + "prometheus", + "rand 0.9.1", + "reqwest", + "serde", + "serde_json", + "solana-client", + "solana-sdk", + "solana-system-interface", + "spl-token-2022-interface", + "thirdweb-core", + "thirdweb-engine", + "tokio", + "tracing", + "tracing-subscriber", + "twmq", + "vault-sdk", + "vault-types", +] + [[package]] name = "engine-solana-core" version = "0.1.0" @@ -4611,9 +4643,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" dependencies = [ "num_enum_derive", "rustversion", @@ -4621,9 +4653,9 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -8261,9 +8293,9 @@ dependencies = [ [[package]] name = "spl-token-2022-interface" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0888304af6b3d839e435712e6c84025e09513017425ff62045b6b8c41feb77d9" +checksum = "2fcd81188211f4b3c8a5eba7fd534c7142f9dd026123b3472492782cc72f4dc6" dependencies = [ "arrayref", "bytemuck", @@ -8577,11 +8609,14 @@ dependencies = [ "anyhow", "aws-arn", "axum", + "base64 0.22.1", + "bincode 2.0.1", "config", "engine-aa-core", "engine-core", "engine-eip7702-core", "engine-executors", + "engine-solana-core", "futures", "moka", "prometheus", @@ -8591,6 +8626,8 @@ dependencies = [ "serde-bool", "serde_json", "serde_with", + "solana-client", + "solana-sdk", "thirdweb-core", "thiserror 2.0.17", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 6b14f68..b8fc7cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "solana-core", "thirdweb-core", "twmq", + "integration-tests", ] resolver = "2" @@ -28,7 +29,9 @@ solana-transaction-status = "3.0" solana-connection-cache = "3.0" solana-commitment-config = "3.0" solana-compute-budget-interface = "3.0" +solana-system-interface = { version = "2.0", features = ["bincode"] } spl-memo-interface = "2.0" +spl-token-2022-interface = "2.1" # AWS aws-config = "1.8.2" diff --git a/integration-tests/.gitignore b/integration-tests/.gitignore new file mode 100644 index 0000000..adb026f --- /dev/null +++ b/integration-tests/.gitignore @@ -0,0 +1,2 @@ +/target +*local.yaml diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml new file mode 100644 index 0000000..36e91c0 --- /dev/null +++ b/integration-tests/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "engine-integration-tests" +version = "0.1.0" +edition = "2024" + +[lib] +test = false # Don't build unit test harness for src/lib.rs etc. +doctest = false # Don't run doc tests for the library + +[dependencies] +anyhow = { workspace = true } +tokio = { workspace = true, features = ["full"] } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } +serde = { workspace = true } +serde_json = { workspace = true } +base64 = { workspace = true } +bincode = { workspace = true } +moka = { workspace = true } +reqwest = { workspace = true, features = ["json"] } +prometheus = { workspace = true } +config = { workspace = true } + +# Solana dependencies +solana-sdk = { workspace = true, features = ["full"] } +solana-client = { workspace = true } +solana-system-interface = { workspace = true } +spl-token-2022-interface = { workspace = true } + +# Vault/Engine dependencies +vault-sdk = { workspace = true } +vault-types = { workspace = true } +engine-solana-core = { path = "../solana-core" } +engine-core = { path = "../core" } +engine-executors = { path = "../executors" } +thirdweb-core = { path = "../thirdweb-core" } + +# Testing utilities +once_cell = "1.20" + +[dev-dependencies] +thirdweb-engine = { path = "../server" } +twmq = { path = "../twmq" } +rand = { workspace = true } diff --git a/integration-tests/README.md b/integration-tests/README.md new file mode 100644 index 0000000..1757896 --- /dev/null +++ b/integration-tests/README.md @@ -0,0 +1,312 @@ +# Engine Integration Tests + +This crate contains integration tests for the Engine server with **full code coverage**. Tests programmatically start the Engine server within the test process, following the same pattern as the Vault integration tests. + +## Quick Start + +### 1. Copy and configure test settings + +```bash +cd integration-tests/configuration +cp test_local.yaml.example test_local.yaml +# Edit test_local.yaml and fill in your credentials +``` + +### 2. Start required services + +```bash +# Start Redis +redis-server + +# Start Vault (or use remote Vault) +# Update vault.url in test_local.yaml +``` + +### 3. Run tests + +```bash +cargo test -p engine-integration-tests +``` + +## Configuration + +Tests use YAML configuration files (like the server does): + +- **`test_base.yaml`** - Base configuration with defaults and structure +- **`test_local.yaml`** - Local overrides (create from `.example` file) + +### Configuration Structure + +```yaml +vault: + url: http://127.0.0.1:3001 + +redis: + url: redis://127.0.0.1:6379 + +thirdweb: + secret_key: YOUR_SECRET_KEY_HERE # Required! + client_id: YOUR_CLIENT_ID_HERE # Required! + urls: + rpc: https://rpc.thirdweb-dev.com + bundler: https://bundler.thirdweb-dev.com + # ... other URLs + +solana: + devnet: + http_url: https://api.devnet.solana.com + ws_url: wss://api.devnet.solana.com + # ... mainnet, local + +queue: + # Test-optimized queue configuration + webhook_workers: 1 + solana_executor_workers: 1 + # ... other workers +``` + +### Environment Variable Overrides + +You can override any config value with environment variables using `TEST_` prefix: + +```bash +# Override vault URL +export TEST__VAULT__URL=http://localhost:3001 + +# Override thirdweb secret +export TEST__THIRDWEB__SECRET_KEY=your_key + +# Run tests +cargo test -p engine-integration-tests +``` + +## Architecture + +The tests import and start Engine server components directly: +- **No external server required** - server is started programmatically +- **Full code coverage** - all server code runs in the test process +- **Isolated environments** - each test gets its own server instance +- **Proper cleanup** - resources are automatically cleaned up after tests + +## Prerequisites + +### Required Services + +1. **Redis**: Tests require a running Redis instance + ```bash + docker run -d -p 6379:6379 redis:latest + # or + redis-server + ``` + +2. **Vault**: Tests require access to a Vault instance + ```bash + # Configure vault.url in test_local.yaml + ``` + +### Required Configuration + +Fill in these values in `configuration/test_local.yaml`: + +- ✅ **`thirdweb.secret_key`** - Get from https://thirdweb.com/dashboard +- ✅ **`thirdweb.client_id`** - Get from https://thirdweb.com/dashboard +- ✅ **`vault.url`** - Your Vault instance URL +- ✅ **`redis.url`** - Your Redis instance URL + +## Running Tests + +### Run all integration tests + +```bash +cargo test -p engine-integration-tests +``` + +### Run specific test + +```bash +cargo test -p engine-integration-tests test_partial_signature_spl_transfer +``` + +### Run with full logging + +```bash +RUST_LOG=debug cargo test -p engine-integration-tests -- --nocapture +``` + +### Use different environment + +```bash +# Use test_staging.yaml instead of test_local.yaml +TEST_ENVIRONMENT=staging cargo test -p engine-integration-tests +``` + +## Test Scenarios + +### 1. Partial Signature Support (`test_partial_signature_spl_transfer`) + +This test demonstrates the core partial signature flow: + +1. **Test Environment Setup**: Programmatically starts Engine server using config files +2. **Vault Wallet Creation**: Creates a service account and Solana wallet in Vault +3. **Transaction Construction**: Builds a system transfer transaction where the Vault wallet is fee payer +4. **HTTP API Call**: Sends unsigned transaction to Engine's `/v1/solana/sign/transaction` endpoint +5. **Vault Signing**: Engine uses Vault to sign the transaction +6. **Verification**: Confirms the signature is valid and properly positioned +7. **Broadcast Ready**: Transaction is fully signed and ready for broadcast + +**What it tests:** +- Server initialization and routing +- Configuration loading from YAML files +- HTTP API endpoint for transaction signing +- Vault integration for key management +- Partial signature scenarios +- Transaction serialization/deserialization (bincode + base64) +- Signature verification using Solana SDK v3 + +### 2. Signature Verification (`test_transaction_signature_verification`) + +Tests that: +- Signatures are properly applied to transactions +- Signature format is correct (base58) +- Signatures can be parsed and validated +- Signed transactions match expected structure +- Non-default signatures indicate proper signing + +### 3. Error Handling (`test_invalid_transaction_handling`) + +Tests error cases: +- Invalid base64 transaction data +- Malformed requests +- Proper error response formatting +- HTTP status codes + +## Configuration Files + +``` +integration-tests/ +├── configuration/ +│ ├── test_base.yaml # Base config with defaults +│ ├── test_local.yaml.example # Example local config +│ └── test_local.yaml # Your local config (git-ignored) +├── src/ +│ └── lib.rs # Helper functions +└── tests/ + ├── setup.rs # TestEnvironment + config loading + └── sign_solana_transaction.rs # Test cases +``` + +## How It Works + +### Configuration Loading + +The `TestEnvironment` loads configuration from YAML files: + +```rust +let env = TestEnvironment::new("test_name").await?; +``` + +This: +1. Loads `test_base.yaml` for defaults +2. Merges `test_local.yaml` for overrides +3. Applies environment variable overrides (`TEST__*`) +4. Initializes all Engine components from config +5. Starts HTTP server on random available port +6. Returns environment ready for testing + +### Test Flow + +```rust +#[tokio::test] +async fn my_test() -> Result<()> { + // Start server programmatically (uses YAML config) + let env = TestEnvironment::new("my_test").await?; + + // Create test wallet in Vault + let (admin_key, wallet) = create_test_solana_wallet(env.vault_client()).await?; + + // Make HTTP requests to env.server_url() + let response = client + .post(&format!("{}/v1/solana/sign/transaction", env.server_url())) + .header("x-vault-access-token", format!("Bearer {}", admin_key)) + .json(&request) + .send() + .await?; + + // Test assertions... + Ok(()) +} +``` + +## Solana SDK v3 Features Used + +This test suite uses **Solana SDK v3** features according to the [migration guide](https://github.com/anza-xyz/solana-sdk): + +- `Address` type (though `Pubkey` remains as type alias) +- `VersionedTransaction` for modern transaction format +- `v0::Message` for versioned message construction +- `solana-system-interface` v2 (compatible with SDK v3) +- Proper signature verification with SDK v3 APIs +- `Hash::as_bytes()` for hash access (private inner bytes in v3) + +## Troubleshooting + +### "Failed to build test configuration" error + +- Ensure `configuration/test_local.yaml` exists +- Copy from `test_local.yaml.example` if needed +- Check YAML syntax is valid + +### "Failed to connect to Redis" error + +- Ensure Redis is running: `redis-cli ping` should return `PONG` +- Check `redis.url` in configuration +- For tests: `docker run -d -p 6379:6379 redis:latest` + +### "Failed to create Vault client" error + +- Ensure Vault server is running +- Check `vault.url` in configuration +- Verify network connectivity to Vault + +### "Missing thirdweb credentials" errors + +- Fill in `thirdweb.secret_key` in `test_local.yaml` +- Fill in `thirdweb.client_id` in `test_local.yaml` +- Get credentials from https://thirdweb.com/dashboard + +### Tests hang or timeout + +- Check Redis is responsive +- Verify Vault is accessible +- Look for port conflicts (server uses random ports) +- Check logs with `RUST_LOG=debug` + +## Code Coverage + +Since the server runs in-process, all code executed during tests is included in code coverage reports: + +```bash +# Generate coverage report +cargo tarpaulin --out Html --output-dir coverage -p engine-integration-tests + +# Or use llvm-cov +cargo llvm-cov --html -p engine-integration-tests +``` + +This captures: +- Server initialization code +- Configuration loading +- HTTP routing and handlers +- Vault integration code +- Transaction signing logic +- Error handling paths +- Serialization/deserialization + +## Future Enhancements + +- [ ] Add tests for transaction broadcast to local Solana validator +- [ ] Test different chain IDs (mainnet, devnet, testnet, local) +- [ ] Add performance/benchmarking tests +- [ ] Test error scenarios (rate limits, network failures) +- [ ] Add tests for compute budget instructions +- [ ] Test with address lookup tables (v0 transactions) diff --git a/integration-tests/configuration/test_base.yaml b/integration-tests/configuration/test_base.yaml new file mode 100644 index 0000000..431eba5 --- /dev/null +++ b/integration-tests/configuration/test_base.yaml @@ -0,0 +1,56 @@ +# Base configuration for Engine integration tests +# This file contains default values and structure +# Override specific values in test_local.yaml + +# Vault configuration +vault: + # REQUIRED: Admin key or access token for signing + admin_key: null + # REQUIRED: Pre-existing Solana wallet address to use for tests + # This wallet must exist in Vault and be accessible with the admin_key + # Example: 5ZWj7a1f8tWkjBESHKgrLmXshuXxqeY9SYcfbshpAqPG + test_solana_wallet: null + +# Redis configuration for test queues +redis: + url: redis://127.0.0.1:6379 + +# Thirdweb service configuration +thirdweb: + urls: + rpc: rpc.thirdweb-dev.com + bundler: bundler.thirdweb-dev.com + paymaster: bundler.thirdweb-dev.com + vault: https://d2w4ge7u2axqfk.cloudfront.net + abi_service: https://contract.thirdweb.com/abi/ + iaw_service: https://embedded-wallet.thirdweb-dev.com + secret: read_from_env + client_id: read_from_env + +# Solana RPC URLs for testing +solana: + devnet: + http_url: https://api.devnet.solana.com + ws_url: wss://api.devnet.solana.com + mainnet: + http_url: https://api.mainnet-beta.solana.com + ws_url: wss://api.mainnet-beta.solana.com + local: + http_url: http://127.0.0.1:8899 + ws_url: ws://127.0.0.1:8900 + +# Test queue configuration (minimal for tests) +queue: + webhook_workers: 1 + external_bundler_send_workers: 1 + userop_confirm_workers: 1 + eoa_executor_workers: 1 + solana_executor_workers: 1 + local_concurrency: 1 + polling_interval_ms: 100 + lease_duration_seconds: 10 + monitoring: + eoa_send_degradation_threshold_seconds: 10 + eoa_confirmation_degradation_threshold_seconds: 120 + eoa_stuck_threshold_seconds: 600 + diff --git a/integration-tests/src/lib.rs b/integration-tests/src/lib.rs new file mode 100644 index 0000000..9d3acaa --- /dev/null +++ b/integration-tests/src/lib.rs @@ -0,0 +1,115 @@ +use solana_sdk::{ + hash::Hash, + instruction::Instruction, + message::{v0, VersionedMessage}, + pubkey::Pubkey, + signature::Signature, + transaction::VersionedTransaction, +}; +use std::str::FromStr; + +/// Helper function to create a simple SPL token transfer transaction +/// This creates a transaction that requires multiple signers (partial signature scenario) +pub fn create_spl_token_transfer_transaction( + fee_payer: Pubkey, // Engine wallet (will sign second) + token_authority: Pubkey, // User wallet (will sign first) + token_account: Pubkey, // Source token account + destination: Pubkey, // Destination token account + mint: Pubkey, // Token mint address + amount: u64, + decimals: u8, + recent_blockhash: Hash, +) -> Result { + // SPL Token 2022 program ID + let token_program_id = Pubkey::from_str("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")?; + + // Use the proper spl-token-2022-interface helper for transfer_checked + // This ensures correct instruction layout with decimals and proper discriminator + let transfer_ix = spl_token_2022_interface::instruction::transfer_checked( + &token_program_id, + &token_account, // Source account + &mint, // Token mint + &destination, // Destination account + &token_authority, // Authority (will be a required signer) + &[], // No multisig signers + amount, + decimals, + )?; + + // Create the message with fee payer + let message = v0::Message::try_compile( + &fee_payer, // Fee payer (engine wallet) + &[transfer_ix], + &[], // No address lookup tables + recent_blockhash, + )?; + + let message = VersionedMessage::V0(message); + + // Calculate number of required signatures + let num_signatures = message.header().num_required_signatures as usize; + + // Initialize with default (empty) signatures + let signatures = vec![Signature::default(); num_signatures]; + + Ok(VersionedTransaction { + signatures, + message, + }) +} + +/// Helper function to verify transaction signature +/// Returns true if the signature at the given index is valid +pub fn verify_signature( + transaction: &VersionedTransaction, + signer_pubkey: &Pubkey, +) -> bool { + // Find the signer's index in the account keys + let account_keys = transaction.message.static_account_keys(); + + let signer_index = account_keys + .iter() + .position(|key| key == signer_pubkey); + + if let Some(index) = signer_index && index < transaction.signatures.len() { + // Check if the signature is not default (i.e., has been signed) + let sig = &transaction.signatures[index]; + return sig != &Signature::default(); + } + + false +} + +/// Parse a Solana public key from string +pub fn parse_pubkey(pubkey_str: &str) -> Result { + Ok(Pubkey::from_str(pubkey_str)?) +} + +/// Create a simple system transfer transaction for testing +pub fn create_system_transfer( + from: Pubkey, + to: Pubkey, + lamports: u64, + recent_blockhash: Hash, +) -> Result { + use solana_system_interface::instruction as system_instruction; + + let transfer_ix = system_instruction::transfer(&from, &to, lamports); + + let message = v0::Message::try_compile( + &from, + &[transfer_ix], + &[], + recent_blockhash, + )?; + + let message = VersionedMessage::V0(message); + let num_signatures = message.header().num_required_signatures as usize; + let signatures = vec![Signature::default(); num_signatures]; + + Ok(VersionedTransaction { + signatures, + message, + }) +} + diff --git a/integration-tests/tests/setup.rs b/integration-tests/tests/setup.rs new file mode 100644 index 0000000..9262cb1 --- /dev/null +++ b/integration-tests/tests/setup.rs @@ -0,0 +1,374 @@ +use anyhow::{Context, Result}; +use config::{Config, File}; +use engine_core::{ + credentials::KmsClientCache, + signer::{EoaSigner, SolanaSigner}, + userop::UserOpSigner, +}; +use engine_executors::{ + eoa::authorization_cache::EoaAuthorizationCache, + metrics::{ExecutorMetrics, initialize_metrics}, + solana_executor::rpc_cache::{SolanaRpcCache, SolanaRpcUrls}, +}; +use serde::Deserialize; +use std::{env, sync::Arc, time::Duration}; +use thirdweb_core::{abi::ThirdwebAbiServiceBuilder, auth::ThirdwebAuth, iaw::IAWClient}; +use thirdweb_engine::{ + EngineServer, EngineServerState, QueueManager, ThirdwebChainService, + // Import config types instead of duplicating them + ThirdwebConfig, RedisConfig as ServerRedisConfig, + SolanaConfig, QueueConfig, +}; +use tokio::net::TcpListener; +use tracing::info; + +/// Test configuration structure matching test_base.yaml +/// Reuses types from server config to avoid duplication +#[derive(Debug, Clone, Deserialize)] +pub struct TestConfig { + pub vault: VaultConfig, + pub redis: ServerRedisConfig, + pub thirdweb: ThirdwebConfig, + pub solana: SolanaConfig, + pub queue: QueueConfig, +} + +/// Vault-specific configuration for tests +#[derive(Debug, Clone, Deserialize)] +pub struct VaultConfig { + /// Admin key for signing transactions + pub admin_key: Option, + /// Pre-existing Solana wallet to use for tests (REQUIRED) + pub test_solana_wallet: Option, +} + +/// Load test configuration from YAML files +pub fn load_test_config() -> Result { + let base_path = env::current_dir().context("Failed to determine current directory")?; + let config_dir = base_path.join("configuration"); + + // Detect environment (default to local) + let environment = env::var("TEST_ENVIRONMENT").unwrap_or_else(|_| "local".to_string()); + + info!( + "Loading test configuration for environment: {}", + environment + ); + + let config = Config::builder() + .add_source(File::from(config_dir.join("test_base.yaml"))) + .add_source( + File::from(config_dir.join(format!("test_{}.yaml", environment))).required(false), // Optional, falls back to base if not present + ) + // Allow environment variable overrides with TEST_ prefix + .add_source(config::Environment::with_prefix("TEST").separator("__")) + .build() + .context("Failed to build test configuration")?; + + let test_config = config + .try_deserialize::() + .context("Failed to deserialize test configuration")?; + + info!("Test configuration loaded successfully"); + Ok(test_config) +} + +/// Find an available port +fn find_available_port() -> u16 { + let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let port = listener.local_addr().unwrap().port(); + drop(listener); + port +} + +/// Test environment for integration tests +#[allow(dead_code)] +pub struct TestEnvironment { + /// Server port + pub server_port: u16, + + /// Engine server instance + engine_server: Arc, + + /// Vault client for creating test wallets + pub vault_client: Arc, + + /// Server URL for HTTP requests + pub server_url: String, + + /// Test configuration + pub config: TestConfig, +} + +impl TestEnvironment { + /// Create a new test environment with isolated components + pub async fn new(test_name: &str) -> Result { + init_logging(); + + info!("Setting up test environment for: {}", test_name); + + // Load configuration from YAML files + let config = load_test_config()?; + + // Validate required configuration + if config.vault.admin_key.is_none() { + anyhow::bail!( + "vault.admin_key is required in test configuration. \ + Please set it in integration-tests/configuration/test_local.yaml" + ); + } + + if config.vault.test_solana_wallet.is_none() { + anyhow::bail!( + "vault.test_solana_wallet is required in test configuration. \ + This should be a Solana wallet address that exists in your Vault. \ + Please set it in integration-tests/configuration/test_local.yaml" + ); + } + + info!("Configuration loaded and validated"); + + // Create vault client + let vault_client = vault_sdk::VaultClient::builder(config.thirdweb.urls.vault.clone()) + .build() + .await + .context("Failed to create Vault client")?; + + info!("Vault client initialized: {}", config.thirdweb.urls.vault); + + // Setup components using config + let chains = Arc::new(ThirdwebChainService { + secret_key: config.thirdweb.secret.clone(), + client_id: config.thirdweb.client_id.clone(), + bundler_base_url: config.thirdweb.urls.bundler.clone(), + paymaster_base_url: config.thirdweb.urls.paymaster.clone(), + rpc_base_url: config.thirdweb.urls.rpc.clone(), + }); + + let iaw_client = IAWClient::new(&config.thirdweb.urls.iaw_service) + .context("Failed to create IAW client")?; + + let kms_client_cache: KmsClientCache = moka::future::Cache::builder() + .max_capacity(100) + .time_to_live(Duration::from_secs(60 * 60)) + .time_to_idle(Duration::from_secs(60 * 30)) + .build(); + + let signer = Arc::new(UserOpSigner { + vault_client: vault_client.clone(), + iaw_client: iaw_client.clone(), + }); + + let eoa_signer = Arc::new(EoaSigner::new(vault_client.clone(), iaw_client.clone())); + let solana_signer = Arc::new(SolanaSigner::new(vault_client.clone(), iaw_client)); + + // Setup Redis + let redis_client = twmq::redis::Client::open(config.redis.url.as_str()) + .context("Failed to connect to Redis")?; + + let authorization_cache = EoaAuthorizationCache::new( + moka::future::Cache::builder() + .max_capacity(1024 * 1024 * 1024) + .time_to_live(Duration::from_secs(60 * 5)) + .time_to_idle(Duration::from_secs(60)) + .build(), + ); + + // Create Solana RPC cache with configured URLs + let solana_rpc_urls = SolanaRpcUrls { + devnet: config.solana.devnet.http_url.clone(), + mainnet: config.solana.mainnet.http_url.clone(), + local: config.solana.local.http_url.clone(), + }; + let solana_rpc_cache = Arc::new(SolanaRpcCache::new(solana_rpc_urls)); + + info!("Solana RPC cache initialized"); + + // Convert test config to engine queue config + let queue_config = thirdweb_engine::QueueConfig { + webhook_workers: config.queue.webhook_workers, + external_bundler_send_workers: config.queue.external_bundler_send_workers, + userop_confirm_workers: config.queue.userop_confirm_workers, + eoa_executor_workers: config.queue.eoa_executor_workers, + solana_executor_workers: config.queue.solana_executor_workers, + execution_namespace: Some(format!("test_{}", test_name)), + local_concurrency: config.queue.local_concurrency, + polling_interval_ms: config.queue.polling_interval_ms, + lease_duration_seconds: config.queue.lease_duration_seconds, + monitoring: thirdweb_engine::config::MonitoringConfig { + eoa_send_degradation_threshold_seconds: config + .queue + .monitoring + .eoa_send_degradation_threshold_seconds, + eoa_confirmation_degradation_threshold_seconds: config + .queue + .monitoring + .eoa_confirmation_degradation_threshold_seconds, + eoa_stuck_threshold_seconds: config.queue.monitoring.eoa_stuck_threshold_seconds, + }, + }; + + let solana_config = thirdweb_engine::SolanaConfig { + devnet: thirdweb_engine::config::SolanRpcConfigData { + http_url: config.solana.devnet.http_url.clone(), + ws_url: config.solana.devnet.ws_url.clone(), + }, + mainnet: thirdweb_engine::config::SolanRpcConfigData { + http_url: config.solana.mainnet.http_url.clone(), + ws_url: config.solana.mainnet.ws_url.clone(), + }, + local: thirdweb_engine::config::SolanRpcConfigData { + http_url: config.solana.local.http_url.clone(), + ws_url: config.solana.local.ws_url.clone(), + }, + }; + + let queue_manager = QueueManager::new( + redis_client.clone(), + &queue_config, + &solana_config, + chains.clone(), + signer.clone(), + eoa_signer.clone(), + authorization_cache.clone(), + kms_client_cache.clone(), + ) + .await + .context("Failed to create queue manager")?; + + info!("Queue manager initialized"); + + let abi_service = ThirdwebAbiServiceBuilder::new( + &config.thirdweb.urls.abi_service, + ThirdwebAuth::SecretKey(config.thirdweb.secret.clone()), + ) + .context("Failed to create ABI service builder")? + .build() + .context("Failed to build ABI service")?; + + let execution_router = thirdweb_engine::ExecutionRouter { + namespace: queue_config.execution_namespace.clone(), + redis: redis_client.get_connection_manager().await?, + authorization_cache, + webhook_queue: queue_manager.webhook_queue.clone(), + external_bundler_send_queue: queue_manager.external_bundler_send_queue.clone(), + userop_confirm_queue: queue_manager.userop_confirm_queue.clone(), + eoa_executor_queue: queue_manager.eoa_executor_queue.clone(), + eip7702_send_queue: queue_manager.eip7702_send_queue.clone(), + eip7702_confirm_queue: queue_manager.eip7702_confirm_queue.clone(), + solana_executor_queue: queue_manager.solana_executor_queue.clone(), + transaction_registry: queue_manager.transaction_registry.clone(), + vault_client: Arc::new(vault_client.clone()), + chains: chains.clone(), + }; + + // Initialize metrics registry and executor metrics + let metrics_registry = Arc::new(prometheus::Registry::new()); + let executor_metrics = + ExecutorMetrics::new(&metrics_registry).expect("Failed to create executor metrics"); + initialize_metrics(executor_metrics); + + info!("Executor metrics initialized"); + + // Create engine server + let mut engine_server = EngineServer::new(EngineServerState { + userop_signer: signer.clone(), + eoa_signer: eoa_signer.clone(), + solana_signer: solana_signer.clone(), + solana_rpc_cache: solana_rpc_cache.clone(), + abi_service: Arc::new(abi_service), + vault_client: Arc::new(vault_client.clone()), + chains, + execution_router: Arc::new(execution_router), + queue_manager: Arc::new(queue_manager), + diagnostic_access_password: None, + metrics_registry, + kms_client_cache: kms_client_cache.clone(), + }) + .await; + + // Find available port and start server + let server_port = find_available_port(); + let server_addr = format!("127.0.0.1:{}", server_port); + let listener = TcpListener::bind(&server_addr) + .await + .context("Failed to bind HTTP server to address")?; + + info!("Starting HTTP server on port {}", server_port); + + engine_server + .start(listener) + .context("Failed to start HTTP server")?; + + let server_url = format!("http://127.0.0.1:{}", server_port); + + // Wait for server to be ready + tokio::time::sleep(Duration::from_millis(500)).await; + + info!("Test environment ready at {}", server_url); + + Ok(Self { + server_port, + engine_server: Arc::new(engine_server), + vault_client: Arc::new(vault_client), + server_url, + config, + }) + } + + /// Get a reference to the vault client + pub fn vault_client(&self) -> &Arc { + &self.vault_client + } + + /// Get the server URL + pub fn server_url(&self) -> &str { + &self.server_url + } + + /// Get the vault admin key from configuration + pub fn vault_admin_key(&self) -> &str { + self.config + .vault + .admin_key + .as_ref() + .expect("vault.admin_key should be validated in new()") + } + + /// Get the test Solana wallet address from configuration + pub fn test_solana_wallet(&self) -> &str { + self.config + .vault + .test_solana_wallet + .as_ref() + .expect("vault.test_solana_wallet should be validated in new()") + } +} + +impl Drop for TestEnvironment { + fn drop(&mut self) { + info!("Cleaning up test environment"); + // Components are automatically cleaned up when their Arc references are dropped + } +} + +/// Initialize logging exactly once at the beginning of testing +pub fn init_logging() { + use once_cell::sync::Lazy; + + static INIT_LOGGER: Lazy<()> = Lazy::new(|| { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::from_default_env() + .add_directive("engine_integration_tests=debug".parse().unwrap()) + .add_directive("thirdweb_engine=debug".parse().unwrap()) + .add_directive("engine_core=debug".parse().unwrap()), + ) + .with_test_writer() + .try_init() + .ok(); // Ignore errors if already initialized + }); + + // Force the lazy evaluation + Lazy::force(&INIT_LOGGER); +} diff --git a/integration-tests/tests/sign_solana_transaction.rs b/integration-tests/tests/sign_solana_transaction.rs new file mode 100644 index 0000000..7f0bf7d --- /dev/null +++ b/integration-tests/tests/sign_solana_transaction.rs @@ -0,0 +1,292 @@ +mod setup; + +use anyhow::Result; +use base64::{Engine, engine::general_purpose::STANDARD as Base64Engine}; +use engine_integration_tests::{create_system_transfer, verify_signature}; +use serde::{Deserialize, Serialize}; +use setup::TestEnvironment; +use solana_sdk::{hash::Hash, pubkey::Pubkey, transaction::VersionedTransaction}; +use std::str::FromStr; +use tracing::info; + +/// Request body for signing Solana transactions +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct SignSolanaTransactionRequest { + #[serde(flatten)] + input: TransactionInput, + execution_options: SolanaExecutionOptions, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +enum TransactionInput { + Serialized { transaction: String }, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct SolanaExecutionOptions { + chain_id: String, + signer_address: String, + #[serde(skip_serializing_if = "Option::is_none")] + compute_unit_limit: Option, +} + +/// Response from the sign endpoint +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct SignSolanaTransactionResponse { + result: SignResult, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct SignResult { + signature: String, + signed_transaction: String, +} + +/// Test partial signature support for Solana transactions +/// +/// This test demonstrates the following flow: +/// 1. Create a Solana wallet in Vault (engine wallet) +/// 2. Build a system transfer transaction where engine wallet is fee payer +/// 3. Send to engine to sign with fee payer wallet (via HTTP) +/// 4. Verify the signature is present and valid +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn test_partial_signature_spl_transfer() -> Result<()> { + info!("Starting partial signature test for Solana SPL transfer"); + + // Create test environment (starts server programmatically) + let env = TestEnvironment::new("partial_signature").await?; + + // Get admin key and wallet address from configuration + let admin_key = env.vault_admin_key(); + let engine_pubkey = Pubkey::from_str(env.test_solana_wallet())?; + + info!( + "Using configured Solana wallet as fee payer: {}", + engine_pubkey + ); + + // Step 1: Create a simple transfer transaction (engine wallet pays fees) + let recipient = Pubkey::new_unique(); + let recent_blockhash = Hash::default(); // In real test, fetch from RPC + + info!("Creating transaction with fee payer: {}", engine_pubkey); + let transaction = create_system_transfer( + engine_pubkey, // Fee payer (engine wallet) + recipient, + 1_000_000, // 0.001 SOL + recent_blockhash, + )?; + + info!( + "Transaction created with {} signatures needed", + transaction.signatures.len() + ); + + // Step 2: Serialize the unsigned transaction to send to engine + let tx_bytes = bincode::serde::encode_to_vec(&transaction, bincode::config::standard())?; + let tx_base64 = Base64Engine.encode(&tx_bytes); + + info!( + "Serialized transaction to base64 (length: {} bytes)", + tx_bytes.len() + ); + + // Step 3: Call the engine API to sign the transaction + let client = reqwest::Client::new(); + let sign_url = format!("{}/v1/solana/sign/transaction", env.server_url()); + + let request_body = SignSolanaTransactionRequest { + input: TransactionInput::Serialized { + transaction: tx_base64, + }, + execution_options: SolanaExecutionOptions { + chain_id: "solana:devnet".to_string(), + signer_address: engine_pubkey.to_string(), + compute_unit_limit: None, + }, + }; + + info!("Sending transaction to engine for signing..."); + + let response = client + .post(&sign_url) + .header("x-vault-access-token", admin_key) + .header("Content-Type", "application/json") + .json(&request_body) + .send() + .await?; + + let status = response.status(); + info!("Received response with status: {}", status); + + if !status.is_success() { + let error_body = response.text().await?; + tracing::error!("Error response body: {}", error_body); + anyhow::bail!("Request failed with status {}: {}", status, error_body); + } + + let sign_response: SignSolanaTransactionResponse = response.json().await?; + info!( + "Transaction signed! Signature: {}", + sign_response.result.signature + ); + + // Step 4: Deserialize the signed transaction + let signed_tx_bytes = Base64Engine.decode(&sign_response.result.signed_transaction)?; + let (signed_transaction, _): (VersionedTransaction, _) = + bincode::serde::decode_from_slice(&signed_tx_bytes, bincode::config::standard())?; + + info!("Deserialized signed transaction"); + info!( + "Number of signatures: {}", + signed_transaction.signatures.len() + ); + + // Step 5: Verify the signature + let engine_sig_valid = verify_signature(&signed_transaction, &engine_pubkey); + info!("Engine wallet signature valid: {}", engine_sig_valid); + + assert!( + engine_sig_valid, + "Engine wallet signature should be present and valid" + ); + + // Step 6: Verify transaction structure + let account_keys = signed_transaction.message.static_account_keys(); + info!("Transaction account keys: {:?}", account_keys); + + assert_eq!( + account_keys[0], engine_pubkey, + "First account should be the fee payer (engine wallet)" + ); + + info!("✅ Partial signature test passed!"); + info!("Transaction is properly signed and ready for broadcast"); + + Ok(()) +} + +/// Test transaction signature verification +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn test_transaction_signature_verification() -> Result<()> { + info!("Starting signature verification test"); + + let env = TestEnvironment::new("signature_verification").await?; + let admin_key = env.vault_admin_key(); + let engine_pubkey = Pubkey::from_str(env.test_solana_wallet())?; + + let recipient = Pubkey::new_unique(); + let recent_blockhash = Hash::default(); + + // Create and sign transaction + let transaction = create_system_transfer(engine_pubkey, recipient, 500_000, recent_blockhash)?; + + let tx_bytes = bincode::serde::encode_to_vec(&transaction, bincode::config::standard())?; + let tx_base64 = Base64Engine.encode(&tx_bytes); + + // Sign via engine + let client = reqwest::Client::new(); + let sign_url = format!("{}/v1/solana/sign/transaction", env.server_url()); + + let request_body = SignSolanaTransactionRequest { + input: TransactionInput::Serialized { + transaction: tx_base64, + }, + execution_options: SolanaExecutionOptions { + chain_id: "solana:devnet".to_string(), + signer_address: engine_pubkey.to_string(), + compute_unit_limit: None, + }, + }; + + let response = client + .post(&sign_url) + .header("x-vault-access-token", admin_key) + .header("Content-Type", "application/json") + .json(&request_body) + .send() + .await?; + + let status = response.status(); + if !status.is_success() { + let error_body = response.text().await?; + anyhow::bail!( + "Request failed with status {}: {}", + status, + error_body + ); + } + + let sign_response: SignSolanaTransactionResponse = response.json().await?; + + // Deserialize and verify + let signed_tx_bytes = Base64Engine.decode(&sign_response.result.signed_transaction)?; + let (signed_transaction, _): (VersionedTransaction, _) = + bincode::serde::decode_from_slice(&signed_tx_bytes, bincode::config::standard())?; + + // Verify signature is not default (has been signed) + assert!( + verify_signature(&signed_transaction, &engine_pubkey), + "Signature should be valid" + ); + + // Verify we can parse the signature string + let signature = solana_sdk::signature::Signature::from_str(&sign_response.result.signature)?; + assert_ne!( + signature, + solana_sdk::signature::Signature::default(), + "Signature should not be default" + ); + + info!("✅ Signature verification test passed!"); + + Ok(()) +} + +/// Test error handling for invalid transactions +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn test_invalid_transaction_handling() -> Result<()> { + info!("Starting invalid transaction test"); + + let env = TestEnvironment::new("invalid_transaction").await?; + let admin_key = env.vault_admin_key(); + let engine_pubkey = Pubkey::from_str(env.test_solana_wallet())?; + + // Send invalid base64 + let client = reqwest::Client::new(); + let sign_url = format!("{}/v1/solana/sign/transaction", env.server_url()); + + let request_body = SignSolanaTransactionRequest { + input: TransactionInput::Serialized { + transaction: "invalid_base64!!!".to_string(), + }, + execution_options: SolanaExecutionOptions { + chain_id: "solana:devnet".to_string(), + signer_address: engine_pubkey.to_string(), + compute_unit_limit: None, + }, + }; + + let response = client + .post(&sign_url) + .header("x-vault-access-token", admin_key) + .header("Content-Type", "application/json") + .json(&request_body) + .send() + .await?; + + assert!( + !response.status().is_success(), + "Invalid transaction should return error" + ); + + info!("✅ Invalid transaction handling test passed!"); + + Ok(()) +} diff --git a/server/Cargo.toml b/server/Cargo.toml index 648ebd9..d568ffb 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -13,6 +13,7 @@ vault-sdk = { workspace = true } vault-types = { workspace = true } engine-core = { path = "../core" } engine-aa-core = { path = "../aa-core" } +engine-solana-core = { path = "../solana-core" } engine-executors = { path = "../executors" } twmq = { path = "../twmq" } thirdweb-core = { path = "../thirdweb-core" } @@ -24,6 +25,10 @@ tracing-subscriber = { workspace = true, features = ["env-filter", "json"] } rand = { workspace = true } futures = { workspace = true } serde-bool = { workspace = true } +base64 = { workspace = true } +bincode = { workspace = true } +solana-sdk = { workspace = true } +solana-client = { workspace = true } aide = { workspace = true, features = [ "axum", "axum-json", diff --git a/server/src/http/routes/mod.rs b/server/src/http/routes/mod.rs index 697ad06..768bfed 100644 --- a/server/src/http/routes/mod.rs +++ b/server/src/http/routes/mod.rs @@ -4,6 +4,7 @@ pub mod contract_read; pub mod contract_write; pub mod sign_message; +pub mod sign_solana_transaction; pub mod sign_typed_data; pub mod solana_transaction; pub mod transaction; diff --git a/server/src/http/routes/sign_solana_transaction.rs b/server/src/http/routes/sign_solana_transaction.rs new file mode 100644 index 0000000..6d117f8 --- /dev/null +++ b/server/src/http/routes/sign_solana_transaction.rs @@ -0,0 +1,137 @@ +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use base64::{Engine, engine::general_purpose::STANDARD as Base64Engine}; +use bincode::config::standard as bincode_standard; +use engine_core::error::EngineError; +use engine_solana_core::transaction::SolanaTransaction; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::http::{ + error::ApiEngineError, + extractors::{EngineJson, SigningCredentialsExtractor}, + server::EngineServerState, + types::SuccessResponse, +}; + +// ===== REQUEST/RESPONSE TYPES ===== + +/// Request to sign a Solana transaction +#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct SignSolanaTransactionRequest { + /// Transaction input (instructions or serialized transaction) + #[serde(flatten)] + pub input: engine_solana_core::transaction::SolanaTransactionInput, + + /// Solana execution options + pub execution_options: engine_core::execution_options::solana::SolanaExecutionOptions, +} + +/// Data returned from successful signing +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, utoipa::ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct SignSolanaTransactionResponse { + /// The signature (base58-encoded) + pub signature: String, + /// The signed serialized transaction (base64-encoded) + pub signed_transaction: String, +} + +// ===== ROUTE HANDLER ===== + +#[utoipa::path( + post, + operation_id = "signSolanaTransaction", + path = "/solana/sign/transaction", + tag = "Solana", + request_body(content = SignSolanaTransactionRequest, description = "Sign Solana transaction request", content_type = "application/json"), + responses( + (status = 200, description = "Successfully signed Solana transaction", body = SuccessResponse, content_type = "application/json"), + ), + params( + ("x-vault-access-token" = Option, Header, description = "Vault access token"), + ) +)] +/// Sign Solana Transaction +/// +/// Sign a Solana transaction without broadcasting it +pub async fn sign_solana_transaction( + State(state): State, + SigningCredentialsExtractor(signing_credential): SigningCredentialsExtractor, + EngineJson(request): EngineJson, +) -> Result { + let chain_id = request.execution_options.chain_id; + let signer_address = request.execution_options.signer_address; + + tracing::info!( + chain_id = %chain_id.as_str(), + signer = %signer_address, + "Processing Solana transaction signing request" + ); + + // Get RPC client from cache (same as executor) + let rpc_client = state.solana_rpc_cache.get_or_create(chain_id).await; + + // Get recent blockhash + let recent_blockhash = rpc_client + .get_latest_blockhash() + .await + .map_err(|e| { + ApiEngineError(EngineError::ValidationError { + message: format!("Failed to get recent blockhash: {}", e), + }) + })?; + + // Build the transaction + let solana_tx = SolanaTransaction { + input: request.input, + compute_unit_limit: request.execution_options.compute_unit_limit, + compute_unit_price: None, // Will be set if priority fee is configured + }; + + // Convert to versioned transaction + let versioned_tx = solana_tx + .to_versioned_transaction(signer_address, recent_blockhash) + .map_err(|e| { + ApiEngineError(EngineError::ValidationError { + message: format!("Failed to build transaction: {}", e), + }) + })?; + + // Sign the transaction + let signed_tx = state + .solana_signer + .sign_transaction(versioned_tx, signer_address, &signing_credential) + .await + .map_err(ApiEngineError)?; + + // Get the signature (first signature in the transaction) + let signature = signed_tx.signatures[0]; + + // Serialize the signed transaction to base64 + let signed_tx_bytes = bincode::serde::encode_to_vec(&signed_tx, bincode_standard()).map_err( + |e| { + ApiEngineError(EngineError::ValidationError { + message: format!("Failed to serialize signed transaction: {}", e), + }) + }, + )?; + let signed_tx_base64 = Base64Engine.encode(&signed_tx_bytes); + + let response = SignSolanaTransactionResponse { + signature: signature.to_string(), + signed_transaction: signed_tx_base64, + }; + + tracing::info!( + chain_id = %chain_id.as_str(), + signature = %signature, + "Solana transaction signed successfully" + ); + + Ok((StatusCode::OK, Json(SuccessResponse::new(response)))) +} diff --git a/server/src/http/server.rs b/server/src/http/server.rs index ac3b196..490740c 100644 --- a/server/src/http/server.rs +++ b/server/src/http/server.rs @@ -1,7 +1,8 @@ use std::sync::Arc; use axum::{Json, Router, routing::get}; -use engine_core::{signer::EoaSigner, userop::UserOpSigner, credentials::KmsClientCache}; +use engine_core::{signer::{EoaSigner, SolanaSigner}, userop::UserOpSigner, credentials::KmsClientCache}; +use engine_executors::solana_executor::rpc_cache::SolanaRpcCache; use serde_json::json; use thirdweb_core::abi::ThirdwebAbiService; use tokio::{sync::watch, task::JoinHandle}; @@ -24,6 +25,8 @@ pub struct EngineServerState { pub chains: Arc, pub userop_signer: Arc, pub eoa_signer: Arc, + pub solana_signer: Arc, + pub solana_rpc_cache: Arc, pub abi_service: Arc, pub vault_client: Arc, @@ -66,6 +69,9 @@ impl EngineServer { .routes(routes!( crate::http::routes::solana_transaction::send_solana_transaction )) + .routes(routes!( + crate::http::routes::sign_solana_transaction::sign_solana_transaction + )) .routes(routes!( crate::http::routes::transaction::cancel_transaction )) diff --git a/server/src/lib.rs b/server/src/lib.rs index a629adb..238a559 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -3,3 +3,13 @@ pub mod config; pub mod execution_router; pub mod http; pub mod queue; + +// Re-export commonly used types for integration tests and external usage +pub use chains::ThirdwebChainService; +pub use config::{ + EngineConfig, MonitoringConfig, QueueConfig, RedisConfig, ServerConfig, SolanaConfig, + SolanRpcConfigData, ThirdwebConfig, ThirdwebUrls, +}; +pub use execution_router::ExecutionRouter; +pub use http::server::{EngineServer, EngineServerState}; +pub use queue::manager::QueueManager; diff --git a/server/src/main.rs b/server/src/main.rs index 42a22d2..94bb060 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,7 +1,7 @@ use std::{sync::Arc, time::Duration}; -use engine_core::{signer::EoaSigner, userop::UserOpSigner, credentials::KmsClientCache}; -use engine_executors::{eoa::authorization_cache::EoaAuthorizationCache, metrics::{ExecutorMetrics, initialize_metrics}}; +use engine_core::{signer::{EoaSigner, SolanaSigner}, userop::UserOpSigner, credentials::KmsClientCache}; +use engine_executors::{eoa::authorization_cache::EoaAuthorizationCache, metrics::{ExecutorMetrics, initialize_metrics}, solana_executor::rpc_cache::{SolanaRpcCache, SolanaRpcUrls}}; use thirdweb_core::{abi::ThirdwebAbiServiceBuilder, auth::ThirdwebAuth, iaw::IAWClient}; use thirdweb_engine::{ chains::ThirdwebChainService, @@ -60,7 +60,8 @@ async fn main() -> anyhow::Result<()> { vault_client: vault_client.clone(), iaw_client: iaw_client.clone(), }); - let eoa_signer = Arc::new(EoaSigner::new(vault_client.clone(), iaw_client)); + let eoa_signer = Arc::new(EoaSigner::new(vault_client.clone(), iaw_client.clone())); + let solana_signer = Arc::new(SolanaSigner::new(vault_client.clone(), iaw_client)); let redis_client = twmq::redis::Client::open(config.redis.url.as_str())?; let authorization_cache = EoaAuthorizationCache::new( @@ -71,6 +72,15 @@ async fn main() -> anyhow::Result<()> { .build(), ); + // Create Solana RPC cache with configured URLs + let solana_rpc_urls = SolanaRpcUrls { + devnet: config.solana.devnet.http_url.clone(), + mainnet: config.solana.mainnet.http_url.clone(), + local: config.solana.local.http_url.clone(), + }; + let solana_rpc_cache = Arc::new(SolanaRpcCache::new(solana_rpc_urls)); + tracing::info!("Solana RPC cache initialized"); + let queue_manager = QueueManager::new( redis_client.clone(), &config.queue, @@ -124,6 +134,8 @@ async fn main() -> anyhow::Result<()> { let mut server = EngineServer::new(EngineServerState { userop_signer: signer.clone(), eoa_signer: eoa_signer.clone(), + solana_signer: solana_signer.clone(), + solana_rpc_cache: solana_rpc_cache.clone(), abi_service: Arc::new(abi_service), vault_client: Arc::new(vault_client), chains,