Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand All @@ -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",
Expand Down
1 change: 1 addition & 0 deletions server/src/http/routes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
137 changes: 137 additions & 0 deletions server/src/http/routes/sign_solana_transaction.rs
Original file line number Diff line number Diff line change
@@ -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<SignSolanaTransactionResponse>, content_type = "application/json"),
),
params(
("x-vault-access-token" = Option<String>, Header, description = "Vault access token"),
)
)]
/// Sign Solana Transaction
///
/// Sign a Solana transaction without broadcasting it
pub async fn sign_solana_transaction(
State(state): State<EngineServerState>,
SigningCredentialsExtractor(signing_credential): SigningCredentialsExtractor,
EngineJson(request): EngineJson<SignSolanaTransactionRequest>,
) -> Result<impl IntoResponse, ApiEngineError> {
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))))
}
8 changes: 7 additions & 1 deletion server/src/http/server.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -24,6 +25,8 @@ pub struct EngineServerState {
pub chains: Arc<ThirdwebChainService>,
pub userop_signer: Arc<UserOpSigner>,
pub eoa_signer: Arc<EoaSigner>,
pub solana_signer: Arc<SolanaSigner>,
pub solana_rpc_cache: Arc<SolanaRpcCache>,
pub abi_service: Arc<ThirdwebAbiService>,
pub vault_client: Arc<VaultClient>,

Expand Down Expand Up @@ -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
))
Expand Down
18 changes: 15 additions & 3 deletions server/src/main.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down