From 92871e4068a893173035bd5dc43aa7d87c27157a Mon Sep 17 00:00:00 2001 From: Prithvish Date: Tue, 4 Nov 2025 22:13:34 +0530 Subject: [PATCH 1/2] Enhance Solana transaction support and add new dependencies - Introduced `SolanaSigner` to the engine core for improved Solana transaction handling. - Added `base64`, `bincode`, `solana-client`, and `solana-sdk` as dependencies in `Cargo.toml`. - Updated server state to include `solana_signer` and added new route for signing Solana transactions. --- Cargo.lock | 5 + server/Cargo.toml | 5 + server/src/http/routes/mod.rs | 1 + .../http/routes/sign_solana_transaction.rs | 149 ++++++++++++++++++ server/src/http/server.rs | 6 +- server/src/main.rs | 6 +- 6 files changed, 169 insertions(+), 3 deletions(-) create mode 100644 server/src/http/routes/sign_solana_transaction.rs diff --git a/Cargo.lock b/Cargo.lock index 4b0ee3e..fd4d890 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8577,11 +8577,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 +8594,8 @@ dependencies = [ "serde-bool", "serde_json", "serde_with", + "solana-client", + "solana-sdk", "thirdweb-core", "thiserror 2.0.17", "tokio", 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..26ce6a0 --- /dev/null +++ b/server/src/http/routes/sign_solana_transaction.rs @@ -0,0 +1,149 @@ +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, + execution_options::solana::SolanaChainId, +}; +use engine_solana_core::transaction::SolanaTransaction; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use solana_client::nonblocking::rpc_client::RpcClient; + +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 URL for the chain + let rpc_url = get_rpc_url(chain_id); + + // Create RPC client + let rpc_client = RpcClient::new(rpc_url.to_string()); + + // 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)))) +} + +/// Get RPC URL for a Solana chain +fn get_rpc_url(chain_id: SolanaChainId) -> &'static str { + chain_id.default_rpc_url() +} diff --git a/server/src/http/server.rs b/server/src/http/server.rs index ac3b196..9351309 100644 --- a/server/src/http/server.rs +++ b/server/src/http/server.rs @@ -1,7 +1,7 @@ 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 serde_json::json; use thirdweb_core::abi::ThirdwebAbiService; use tokio::{sync::watch, task::JoinHandle}; @@ -24,6 +24,7 @@ pub struct EngineServerState { pub chains: Arc, pub userop_signer: Arc, pub eoa_signer: Arc, + pub solana_signer: Arc, pub abi_service: Arc, pub vault_client: Arc, @@ -66,6 +67,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/main.rs b/server/src/main.rs index 42a22d2..70807e5 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,6 +1,6 @@ use std::{sync::Arc, time::Duration}; -use engine_core::{signer::EoaSigner, userop::UserOpSigner, credentials::KmsClientCache}; +use engine_core::{signer::{EoaSigner, SolanaSigner}, userop::UserOpSigner, credentials::KmsClientCache}; use engine_executors::{eoa::authorization_cache::EoaAuthorizationCache, metrics::{ExecutorMetrics, initialize_metrics}}; use thirdweb_core::{abi::ThirdwebAbiServiceBuilder, auth::ThirdwebAuth, iaw::IAWClient}; use thirdweb_engine::{ @@ -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( @@ -124,6 +125,7 @@ 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(), abi_service: Arc::new(abi_service), vault_client: Arc::new(vault_client), chains, From fb90893b7f8d197d4d62e3667c355b5e96c23fe1 Mon Sep 17 00:00:00 2001 From: Prithvish Date: Thu, 6 Nov 2025 01:27:23 +0530 Subject: [PATCH 2/2] Add Solana RPC cache support in server - Introduced `SolanaRpcCache` to manage RPC URLs for Solana transactions. - Updated server state to include `solana_rpc_cache` for efficient RPC client retrieval. - Refactored transaction signing logic to utilize the new RPC cache, improving performance and reliability. --- .../src/http/routes/sign_solana_transaction.rs | 18 +++--------------- server/src/http/server.rs | 2 ++ server/src/main.rs | 12 +++++++++++- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/server/src/http/routes/sign_solana_transaction.rs b/server/src/http/routes/sign_solana_transaction.rs index 26ce6a0..6d117f8 100644 --- a/server/src/http/routes/sign_solana_transaction.rs +++ b/server/src/http/routes/sign_solana_transaction.rs @@ -5,14 +5,10 @@ use axum::{ }; use base64::{Engine, engine::general_purpose::STANDARD as Base64Engine}; use bincode::config::standard as bincode_standard; -use engine_core::{ - error::EngineError, - execution_options::solana::SolanaChainId, -}; +use engine_core::error::EngineError; use engine_solana_core::transaction::SolanaTransaction; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use solana_client::nonblocking::rpc_client::RpcClient; use crate::http::{ error::ApiEngineError, @@ -77,11 +73,8 @@ pub async fn sign_solana_transaction( "Processing Solana transaction signing request" ); - // Get RPC URL for the chain - let rpc_url = get_rpc_url(chain_id); - - // Create RPC client - let rpc_client = RpcClient::new(rpc_url.to_string()); + // 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 @@ -142,8 +135,3 @@ pub async fn sign_solana_transaction( Ok((StatusCode::OK, Json(SuccessResponse::new(response)))) } - -/// Get RPC URL for a Solana chain -fn get_rpc_url(chain_id: SolanaChainId) -> &'static str { - chain_id.default_rpc_url() -} diff --git a/server/src/http/server.rs b/server/src/http/server.rs index 9351309..490740c 100644 --- a/server/src/http/server.rs +++ b/server/src/http/server.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use axum::{Json, Router, routing::get}; 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}; @@ -25,6 +26,7 @@ pub struct EngineServerState { 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, diff --git a/server/src/main.rs b/server/src/main.rs index 70807e5..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, SolanaSigner}, userop::UserOpSigner, credentials::KmsClientCache}; -use engine_executors::{eoa::authorization_cache::EoaAuthorizationCache, metrics::{ExecutorMetrics, initialize_metrics}}; +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, @@ -72,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, @@ -126,6 +135,7 @@ async fn main() -> anyhow::Result<()> { 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,