Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
149 changes: 149 additions & 0 deletions server/src/http/routes/sign_solana_transaction.rs
Original file line number Diff line number Diff line change
@@ -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<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 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()
}
6 changes: 5 additions & 1 deletion server/src/http/server.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -24,6 +24,7 @@ pub struct EngineServerState {
pub chains: Arc<ThirdwebChainService>,
pub userop_signer: Arc<UserOpSigner>,
pub eoa_signer: Arc<EoaSigner>,
pub solana_signer: Arc<SolanaSigner>,
pub abi_service: Arc<ThirdwebAbiService>,
pub vault_client: Arc<VaultClient>,

Expand Down Expand Up @@ -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
))
Expand Down
6 changes: 4 additions & 2 deletions server/src/main.rs
Original file line number Diff line number Diff line change
@@ -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::{
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 Down Expand Up @@ -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,
Expand Down