use axum::{
extract::State,
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
use log::warn;
use soroban_env_host::xdr::{Limits, ReadXdr, WriteXdr};
use crate::server::actor::{ActorHandle, Command, SimulationReply};
use crate::server::types::{
GetLedgerEntriesParams, GetLedgerEntriesResponse, GetLedgersParams, GetLedgersResponse,
GetTransactionParams, GetTransactionResponse, HealthResponse, JsonRpcError, JsonRpcRequest,
JsonRpcResponse, LatestLedgerResponse, LedgerEntryItem, LedgerInfo, NetworkResponse,
SendTransactionParams, SendTransactionResponse, SimulateHostFunctionResult,
SimulateTransactionParams, SimulateTransactionResponse, SimulationCost, VersionInfoResponse,
};
#[derive(Clone)]
pub(crate) struct AppState {
pub(crate) actor: ActorHandle,
}
const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION");
const COMMIT_HASH: &str = "unknown";
const BUILD_TIMESTAMP: &str = "unknown";
const CAPTIVE_CORE_VERSION: &str = "n/a (forked-RPC mode)";
pub(crate) async fn jsonrpc_handler(
State(state): State<AppState>,
Json(req): Json<JsonRpcRequest>,
) -> Response {
if req.jsonrpc != "2.0" {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": format!("unsupported jsonrpc version: {}", req.jsonrpc),
})),
)
.into_response();
}
let id = req.id.clone();
let response = match dispatch(&state, &req).await {
Ok(value) => JsonRpcResponse::ok(id, value),
Err(err) => JsonRpcResponse::err(id, err),
};
Json(response).into_response()
}
async fn dispatch(
state: &AppState,
req: &JsonRpcRequest,
) -> Result<serde_json::Value, JsonRpcError> {
match req.method.as_str() {
"getHealth" => handle_get_health(state).await,
"getVersionInfo" => handle_get_version_info(state).await,
"getNetwork" => handle_get_network(state).await,
"getLatestLedger" => handle_get_latest_ledger(state).await,
"getLedgers" => handle_get_ledgers(state, &req.params).await,
"getLedgerEntries" => handle_get_ledger_entries(state, &req.params).await,
"simulateTransaction" => handle_simulate_transaction(state, &req.params).await,
"sendTransaction" => handle_send_transaction(state, &req.params).await,
"getTransaction" => handle_get_transaction(state, &req.params).await,
unknown => {
warn!("soroban-fork: unsupported RPC method: {unknown}");
Err(JsonRpcError::method_not_found(unknown))
}
}
}
async fn handle_get_health(state: &AppState) -> Result<serde_json::Value, JsonRpcError> {
let reply = state
.actor
.send(|tx| Command::GetLatestLedger { reply: tx })
.await
.map_err(|e| JsonRpcError::internal_error(e.to_string()))?;
let body = HealthResponse {
status: "healthy",
latest_ledger: reply.sequence,
oldest_ledger: reply.sequence,
ledger_retention_window: 0,
};
serde_json::to_value(body).map_err(|e| JsonRpcError::internal_error(e.to_string()))
}
async fn handle_get_version_info(state: &AppState) -> Result<serde_json::Value, JsonRpcError> {
let reply = state
.actor
.send(|tx| Command::GetNetwork { reply: tx })
.await
.map_err(|e| JsonRpcError::internal_error(e.to_string()))?;
let body = VersionInfoResponse {
version: SERVER_VERSION,
commit_hash: COMMIT_HASH,
build_timestamp: BUILD_TIMESTAMP,
captive_core_version: CAPTIVE_CORE_VERSION,
protocol_version: reply.protocol_version,
};
serde_json::to_value(body).map_err(|e| JsonRpcError::internal_error(e.to_string()))
}
async fn handle_get_network(state: &AppState) -> Result<serde_json::Value, JsonRpcError> {
let reply = state
.actor
.send(|tx| Command::GetNetwork { reply: tx })
.await
.map_err(|e| JsonRpcError::internal_error(e.to_string()))?;
let body = NetworkResponse {
passphrase: reply.passphrase,
protocol_version: reply.protocol_version,
network_id: reply.network_id_hex,
friendbot_url: None,
};
serde_json::to_value(body).map_err(|e| JsonRpcError::internal_error(e.to_string()))
}
async fn handle_get_latest_ledger(state: &AppState) -> Result<serde_json::Value, JsonRpcError> {
let reply = state
.actor
.send(|tx| Command::GetLatestLedger { reply: tx })
.await
.map_err(|e| JsonRpcError::internal_error(e.to_string()))?;
let body = LatestLedgerResponse {
id: reply.id,
sequence: reply.sequence,
protocol_version: reply.protocol_version,
};
serde_json::to_value(body).map_err(|e| JsonRpcError::internal_error(e.to_string()))
}
async fn handle_get_ledgers(
state: &AppState,
params: &serde_json::Value,
) -> Result<serde_json::Value, JsonRpcError> {
let _parsed: GetLedgersParams = if params.is_null() {
GetLedgersParams {
start_ledger: None,
pagination: None,
}
} else {
serde_json::from_value(params.clone())
.map_err(|e| JsonRpcError::invalid_params(format!("getLedgers params: {e}")))?
};
let reply = state
.actor
.send(|tx| Command::GetLedgersPage { reply: tx })
.await
.map_err(|e| JsonRpcError::internal_error(e.to_string()))?;
let close_time_str = reply.close_time.to_string();
let body = GetLedgersResponse {
ledgers: vec![LedgerInfo {
hash: format!("forked-ledger-hash-{}", reply.sequence),
sequence: reply.sequence,
ledger_close_time: close_time_str.clone(),
}],
latest_ledger: reply.sequence,
latest_ledger_close_time: close_time_str.clone(),
oldest_ledger: reply.sequence,
oldest_ledger_close_time: close_time_str,
cursor: String::new(),
};
serde_json::to_value(body).map_err(|e| JsonRpcError::internal_error(e.to_string()))
}
async fn handle_get_ledger_entries(
state: &AppState,
params: &serde_json::Value,
) -> Result<serde_json::Value, JsonRpcError> {
let parsed: GetLedgerEntriesParams = serde_json::from_value(params.clone())
.map_err(|e| JsonRpcError::invalid_params(format!("getLedgerEntries params: {e}")))?;
if parsed.keys.is_empty() {
return Err(JsonRpcError::invalid_params(
"getLedgerEntries: keys array must be non-empty",
));
}
let mut decoded_keys = Vec::with_capacity(parsed.keys.len());
for (i, raw) in parsed.keys.iter().enumerate() {
let bytes = BASE64
.decode(raw)
.map_err(|e| JsonRpcError::invalid_params(format!("keys[{i}]: base64 decode: {e}")))?;
let key = soroban_env_host::xdr::LedgerKey::from_xdr(&bytes, Limits::none())
.map_err(|e| JsonRpcError::invalid_params(format!("keys[{i}]: XDR decode: {e}")))?;
decoded_keys.push(key);
}
let reply = state
.actor
.send(|tx| Command::GetLedgerEntries {
keys: decoded_keys,
reply: tx,
})
.await
.map_err(|e| JsonRpcError::internal_error(e.to_string()))?;
let mut items = Vec::with_capacity(reply.entries.len());
for resolved in reply.entries.into_iter().flatten() {
let (key, entry, live_until) = resolved;
let key_xdr = key
.to_xdr(Limits::none())
.map_err(|e| JsonRpcError::internal_error(format!("encode response LedgerKey: {e}")))?;
let data_xdr = entry.data.to_xdr(Limits::none()).map_err(|e| {
JsonRpcError::internal_error(format!("encode response LedgerEntryData: {e}"))
})?;
items.push(LedgerEntryItem {
key: BASE64.encode(&key_xdr),
xdr: BASE64.encode(&data_xdr),
last_modified_ledger_seq: entry.last_modified_ledger_seq,
live_until_ledger_seq: live_until,
});
}
let body = GetLedgerEntriesResponse {
entries: items,
latest_ledger: reply.latest_ledger,
};
serde_json::to_value(body).map_err(|e| JsonRpcError::internal_error(e.to_string()))
}
async fn handle_simulate_transaction(
state: &AppState,
params: &serde_json::Value,
) -> Result<serde_json::Value, JsonRpcError> {
let parsed: SimulateTransactionParams = serde_json::from_value(params.clone())
.map_err(|e| JsonRpcError::invalid_params(format!("simulateTransaction params: {e}")))?;
let envelope_bytes = BASE64
.decode(&parsed.transaction)
.map_err(|e| JsonRpcError::invalid_params(format!("transaction: base64 decode: {e}")))?;
let envelope =
soroban_env_host::xdr::TransactionEnvelope::from_xdr(&envelope_bytes, Limits::none())
.map_err(|e| JsonRpcError::invalid_params(format!("transaction: XDR decode: {e}")))?;
let transaction_size_bytes: u32 = envelope_bytes.len().try_into().map_err(|_| {
JsonRpcError::invalid_params(format!(
"transaction: envelope too large for u32 ({} bytes)",
envelope_bytes.len()
))
})?;
let (host_function, source_account) = extract_invoke_op(&envelope)?;
let reply = state
.actor
.send(|tx| Command::SimulateTransaction {
host_function,
source_account,
transaction_size_bytes,
reply: tx,
})
.await
.map_err(|e| JsonRpcError::internal_error(e.to_string()))?;
encode_simulation_reply(reply)
}
fn extract_invoke_op(
env: &soroban_env_host::xdr::TransactionEnvelope,
) -> Result<
(
soroban_env_host::xdr::HostFunction,
soroban_env_host::xdr::AccountId,
),
JsonRpcError,
> {
use soroban_env_host::xdr::{
FeeBumpTransactionInnerTx, MuxedAccount, Operation, OperationBody, TransactionEnvelope,
};
let (operations, source) = match env {
TransactionEnvelope::TxV0(_) => {
return Err(JsonRpcError::invalid_params(
"simulateTransaction: V0 transaction envelopes do not support Soroban operations",
));
}
TransactionEnvelope::Tx(tx) => (tx.tx.operations.as_slice(), tx.tx.source_account.clone()),
TransactionEnvelope::TxFeeBump(fb) => match &fb.tx.inner_tx {
FeeBumpTransactionInnerTx::Tx(inner) => (
inner.tx.operations.as_slice(),
inner.tx.source_account.clone(),
),
},
};
if operations.len() != 1 {
return Err(JsonRpcError::invalid_params(format!(
"simulateTransaction: expected exactly 1 operation, got {}",
operations.len()
)));
}
let op: &Operation = &operations[0];
let invoke_op = match &op.body {
OperationBody::InvokeHostFunction(ihf) => ihf,
other => {
return Err(JsonRpcError::invalid_params(format!(
"simulateTransaction: only InvokeHostFunction operations supported, got {other:?}"
)));
}
};
let source_muxed: MuxedAccount = op.source_account.clone().unwrap_or(source);
let source_account = match source_muxed {
MuxedAccount::Ed25519(uint256) => soroban_env_host::xdr::AccountId(
soroban_env_host::xdr::PublicKey::PublicKeyTypeEd25519(uint256),
),
MuxedAccount::MuxedEd25519(muxed) => soroban_env_host::xdr::AccountId(
soroban_env_host::xdr::PublicKey::PublicKeyTypeEd25519(muxed.ed25519),
),
};
Ok((invoke_op.host_function.clone(), source_account))
}
fn encode_simulation_reply(reply: SimulationReply) -> Result<serde_json::Value, JsonRpcError> {
use soroban_env_host::xdr::{SorobanTransactionData, SorobanTransactionDataExt};
let latest_ledger = reply.latest_ledger;
let scval = match reply.result {
Ok(scval) => scval,
Err(error) => {
let body = SimulateTransactionResponse {
transaction_data: None,
min_resource_fee: None,
events: None,
results: None,
cost: None,
latest_ledger,
error: Some(error),
};
return serde_json::to_value(body)
.map_err(|e| JsonRpcError::internal_error(e.to_string()));
}
};
let scval_xdr = scval
.to_xdr(Limits::none())
.map_err(|e| JsonRpcError::internal_error(format!("encode result ScVal: {e}")))?;
let mut auth_b64 = Vec::with_capacity(reply.auth.len());
for entry in &reply.auth {
let bytes = entry
.to_xdr(Limits::none())
.map_err(|e| JsonRpcError::internal_error(format!("encode auth entry: {e}")))?;
auth_b64.push(BASE64.encode(&bytes));
}
let resource_fee = reply.min_resource_fee.unwrap_or(0);
let txn_data = SorobanTransactionData {
ext: SorobanTransactionDataExt::V0,
resources: reply.resources.clone(),
resource_fee,
};
let txn_data_xdr = txn_data
.to_xdr(Limits::none())
.map_err(|e| JsonRpcError::internal_error(format!("encode SorobanTransactionData: {e}")))?;
let mut events_b64 = Vec::new();
for ce in reply.contract_events {
let de = soroban_env_host::xdr::DiagnosticEvent {
in_successful_contract_call: true,
event: ce,
};
let bytes = de
.to_xdr(Limits::none())
.map_err(|e| JsonRpcError::internal_error(format!("encode contract event: {e}")))?;
events_b64.push(BASE64.encode(&bytes));
}
for de in reply.diagnostic_events {
let bytes = de
.to_xdr(Limits::none())
.map_err(|e| JsonRpcError::internal_error(format!("encode diagnostic event: {e}")))?;
events_b64.push(BASE64.encode(&bytes));
}
let cost = reply.mem_bytes.map(|mem| SimulationCost {
cpu_insns: reply.resources.instructions.to_string(),
mem_bytes: mem.to_string(),
});
let body = SimulateTransactionResponse {
transaction_data: Some(BASE64.encode(&txn_data_xdr)),
min_resource_fee: reply.min_resource_fee.map(|n| n.to_string()),
events: Some(events_b64),
results: Some(vec![SimulateHostFunctionResult {
auth: auth_b64,
xdr: BASE64.encode(&scval_xdr),
}]),
cost,
latest_ledger,
error: None,
};
serde_json::to_value(body).map_err(|e| JsonRpcError::internal_error(e.to_string()))
}
async fn handle_send_transaction(
state: &AppState,
params: &serde_json::Value,
) -> Result<serde_json::Value, JsonRpcError> {
let parsed: SendTransactionParams = serde_json::from_value(params.clone())
.map_err(|e| JsonRpcError::invalid_params(format!("sendTransaction params: {e}")))?;
let envelope_bytes = BASE64
.decode(&parsed.transaction)
.map_err(|e| JsonRpcError::invalid_params(format!("transaction: base64 decode: {e}")))?;
let envelope =
soroban_env_host::xdr::TransactionEnvelope::from_xdr(&envelope_bytes, Limits::none())
.map_err(|e| JsonRpcError::invalid_params(format!("transaction: XDR decode: {e}")))?;
let (host_function, source_account) = extract_invoke_op(&envelope)?;
let envelope_b64 = parsed.transaction.clone();
let send_reply = state
.actor
.send(|tx| Command::SendTransaction {
envelope_bytes,
host_function,
source_account,
reply: tx,
})
.await
.map_err(|e| JsonRpcError::internal_error(e.to_string()))?;
let latest = state
.actor
.send(|tx| Command::GetLatestLedger { reply: tx })
.await
.map_err(|e| JsonRpcError::internal_error(e.to_string()))?;
let close_time = state
.actor
.send(|tx| Command::GetLedgersPage { reply: tx })
.await
.map_err(|e| JsonRpcError::internal_error(e.to_string()))?
.close_time;
let (status, error_message) = match &send_reply.receipt.result {
Ok(_) => ("SUCCESS", None),
Err(msg) => ("ERROR", Some(msg.clone())),
};
let body = SendTransactionResponse {
status,
hash: hex_lower(&send_reply.hash),
latest_ledger: latest.sequence,
latest_ledger_close_time: close_time.to_string(),
envelope_xdr: envelope_b64,
error_message,
applied_changes: send_reply.receipt.applied_changes,
};
serde_json::to_value(body).map_err(|e| JsonRpcError::internal_error(e.to_string()))
}
async fn handle_get_transaction(
state: &AppState,
params: &serde_json::Value,
) -> Result<serde_json::Value, JsonRpcError> {
let parsed: GetTransactionParams = serde_json::from_value(params.clone())
.map_err(|e| JsonRpcError::invalid_params(format!("getTransaction params: {e}")))?;
let hash = parse_hex32(&parsed.hash)
.ok_or_else(|| JsonRpcError::invalid_params("hash: must be 64-char hex"))?;
let receipt = state
.actor
.send(|tx| Command::GetTransaction { hash, reply: tx })
.await
.map_err(|e| JsonRpcError::internal_error(e.to_string()))?;
let latest = state
.actor
.send(|tx| Command::GetLatestLedger { reply: tx })
.await
.map_err(|e| JsonRpcError::internal_error(e.to_string()))?;
let body = match receipt {
None => GetTransactionResponse {
status: "NOT_FOUND",
latest_ledger: latest.sequence,
ledger: None,
created_at: None,
envelope_xdr: None,
return_value_xdr: None,
error_message: None,
applied_changes: None,
},
Some(r) => {
let envelope_xdr = Some(BASE64.encode(&r.envelope_bytes));
match &r.result {
Ok(scval) => {
let bytes = scval.to_xdr(Limits::none()).map_err(|e| {
JsonRpcError::internal_error(format!("encode return ScVal: {e}"))
})?;
GetTransactionResponse {
status: "SUCCESS",
latest_ledger: latest.sequence,
ledger: Some(r.ledger),
created_at: Some(r.created_at.to_string()),
envelope_xdr,
return_value_xdr: Some(BASE64.encode(&bytes)),
error_message: None,
applied_changes: Some(r.applied_changes),
}
}
Err(msg) => GetTransactionResponse {
status: "FAILED",
latest_ledger: latest.sequence,
ledger: Some(r.ledger),
created_at: Some(r.created_at.to_string()),
envelope_xdr,
return_value_xdr: None,
error_message: Some(msg.clone()),
applied_changes: Some(r.applied_changes),
},
}
}
};
serde_json::to_value(body).map_err(|e| JsonRpcError::internal_error(e.to_string()))
}
fn hex_lower(bytes: &[u8; 32]) -> String {
let mut s = String::with_capacity(64);
for b in bytes {
s.push_str(&format!("{b:02x}"));
}
s
}
fn parse_hex32(s: &str) -> Option<[u8; 32]> {
if s.len() != 64 {
return None;
}
let mut out = [0u8; 32];
for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
let hi = hex_nibble(chunk[0])?;
let lo = hex_nibble(chunk[1])?;
out[i] = (hi << 4) | lo;
}
Some(out)
}
fn hex_nibble(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}