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::{
AssetWire, CloseLedgersParams, CloseLedgersResponse, EtchParams, GetLedgerEntriesParams,
GetLedgerEntriesResponse, GetLedgersParams, GetLedgersResponse, GetTransactionParams,
GetTransactionResponse, HealthResponse, JsonRpcError, JsonRpcRequest, JsonRpcResponse,
LatestLedgerResponse, LedgerEntryItem, LedgerInfo, NetworkResponse, SendTransactionParams,
SendTransactionResponse, SetBalanceParams, SetBalanceResponse, SetCodeParams, SetCodeResponse,
SetLedgerEntryParams, SetLedgerEntryResponse, SetStorageParams, SimulateHostFunctionResult,
SimulateTransactionParams, SimulateTransactionResponse, SimulationCost, StorageDurability,
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,
"fork_setLedgerEntry" => handle_fork_set_ledger_entry(state, &req.params).await,
"fork_setStorage" => handle_fork_set_storage(state, &req.params).await,
"fork_setCode" => handle_fork_set_code(state, &req.params).await,
"fork_setBalance" => handle_fork_set_balance(state, &req.params).await,
"fork_etch" => handle_fork_etch(state, &req.params).await,
"fork_closeLedgers" => handle_fork_close_ledgers(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()))
}
async fn handle_fork_set_ledger_entry(
state: &AppState,
params: &serde_json::Value,
) -> Result<serde_json::Value, JsonRpcError> {
let parsed: SetLedgerEntryParams = serde_json::from_value(params.clone())
.map_err(|e| JsonRpcError::invalid_params(format!("fork_setLedgerEntry params: {e}")))?;
let key_bytes = BASE64
.decode(&parsed.key)
.map_err(|e| JsonRpcError::invalid_params(format!("key: base64 decode: {e}")))?;
let key = soroban_env_host::xdr::LedgerKey::from_xdr(&key_bytes, Limits::none())
.map_err(|e| JsonRpcError::invalid_params(format!("key: XDR decode: {e}")))?;
let entry_bytes = BASE64
.decode(&parsed.entry)
.map_err(|e| JsonRpcError::invalid_params(format!("entry: base64 decode: {e}")))?;
let entry = soroban_env_host::xdr::LedgerEntry::from_xdr(&entry_bytes, Limits::none())
.map_err(|e| JsonRpcError::invalid_params(format!("entry: XDR decode: {e}")))?;
state
.actor
.send(|tx| Command::SetLedgerEntry {
key,
entry,
live_until: parsed.live_until_ledger_seq,
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 = SetLedgerEntryResponse {
ok: true,
latest_ledger: latest.sequence,
};
serde_json::to_value(body).map_err(|e| JsonRpcError::internal_error(e.to_string()))
}
async fn handle_fork_set_storage(
state: &AppState,
params: &serde_json::Value,
) -> Result<serde_json::Value, JsonRpcError> {
use soroban_env_host::xdr::{
ContractDataDurability, ContractDataEntry, ContractId, ExtensionPoint, Hash, LedgerEntry,
LedgerEntryData, LedgerEntryExt, LedgerKey, LedgerKeyContractData, ScAddress, ScVal,
};
let parsed: SetStorageParams = serde_json::from_value(params.clone())
.map_err(|e| JsonRpcError::invalid_params(format!("fork_setStorage params: {e}")))?;
let contract_strkey = stellar_strkey::Contract::from_string(&parsed.contract).map_err(|e| {
JsonRpcError::invalid_params(format!("contract: not a valid C... strkey: {e}"))
})?;
let contract_address = ScAddress::Contract(ContractId(Hash(contract_strkey.0)));
let key_bytes = BASE64
.decode(&parsed.key)
.map_err(|e| JsonRpcError::invalid_params(format!("key: base64 decode: {e}")))?;
let key_scval = ScVal::from_xdr(&key_bytes, Limits::none())
.map_err(|e| JsonRpcError::invalid_params(format!("key: ScVal XDR decode: {e}")))?;
let value_bytes = BASE64
.decode(&parsed.value)
.map_err(|e| JsonRpcError::invalid_params(format!("value: base64 decode: {e}")))?;
let value_scval = ScVal::from_xdr(&value_bytes, Limits::none())
.map_err(|e| JsonRpcError::invalid_params(format!("value: ScVal XDR decode: {e}")))?;
let durability = match parsed.durability.unwrap_or_default() {
StorageDurability::Persistent => ContractDataDurability::Persistent,
StorageDurability::Temporary => ContractDataDurability::Temporary,
};
let ledger_key = LedgerKey::ContractData(LedgerKeyContractData {
contract: contract_address.clone(),
key: key_scval.clone(),
durability,
});
let ledger_entry = LedgerEntry {
last_modified_ledger_seq: 0,
data: LedgerEntryData::ContractData(ContractDataEntry {
ext: ExtensionPoint::V0,
contract: contract_address,
key: key_scval,
durability,
val: value_scval,
}),
ext: LedgerEntryExt::V0,
};
state
.actor
.send(|tx| Command::SetLedgerEntry {
key: ledger_key,
entry: ledger_entry,
live_until: parsed.live_until_ledger_seq,
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 = SetLedgerEntryResponse {
ok: true,
latest_ledger: latest.sequence,
};
serde_json::to_value(body).map_err(|e| JsonRpcError::internal_error(e.to_string()))
}
async fn handle_fork_set_code(
state: &AppState,
params: &serde_json::Value,
) -> Result<serde_json::Value, JsonRpcError> {
use sha2::{Digest, Sha256};
use soroban_env_host::xdr::{
ContractCodeEntry, ContractCodeEntryExt, Hash, LedgerEntry, LedgerEntryData,
LedgerEntryExt, LedgerKey, LedgerKeyContractCode,
};
let parsed: SetCodeParams = serde_json::from_value(params.clone())
.map_err(|e| JsonRpcError::invalid_params(format!("fork_setCode params: {e}")))?;
let wasm = BASE64
.decode(&parsed.wasm)
.map_err(|e| JsonRpcError::invalid_params(format!("wasm: base64 decode: {e}")))?;
let hash_bytes: [u8; 32] = Sha256::digest(&wasm).into();
let hash = Hash(hash_bytes);
let ledger_key = LedgerKey::ContractCode(LedgerKeyContractCode { hash: hash.clone() });
let ledger_entry = LedgerEntry {
last_modified_ledger_seq: 0,
data: LedgerEntryData::ContractCode(ContractCodeEntry {
ext: ContractCodeEntryExt::V0,
hash,
code: wasm.try_into().map_err(|_| {
JsonRpcError::invalid_params("wasm: bytes exceed XDR BytesM<u32::MAX> capacity")
})?,
}),
ext: LedgerEntryExt::V0,
};
state
.actor
.send(|tx| Command::SetLedgerEntry {
key: ledger_key,
entry: ledger_entry,
live_until: parsed.live_until_ledger_seq,
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 = SetCodeResponse {
ok: true,
hash: hex_lower(&hash_bytes),
latest_ledger: latest.sequence,
};
serde_json::to_value(body).map_err(|e| JsonRpcError::internal_error(e.to_string()))
}
async fn handle_fork_set_balance(
state: &AppState,
params: &serde_json::Value,
) -> Result<serde_json::Value, JsonRpcError> {
use soroban_env_host::xdr::{
AccountEntry, AccountEntryExt, AccountId, AlphaNum12, AlphaNum4, AssetCode12, AssetCode4,
LedgerEntry, LedgerEntryData, LedgerEntryExt, LedgerKey, LedgerKeyAccount,
LedgerKeyTrustLine, PublicKey, SequenceNumber, Thresholds, TrustLineAsset, TrustLineEntry,
TrustLineEntryExt, Uint256,
};
let parsed: SetBalanceParams = serde_json::from_value(params.clone())
.map_err(|e| JsonRpcError::invalid_params(format!("fork_setBalance params: {e}")))?;
let account_strkey =
stellar_strkey::ed25519::PublicKey::from_string(&parsed.account).map_err(|e| {
JsonRpcError::invalid_params(format!("account: not a valid G... strkey: {e}"))
})?;
let account_id = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(account_strkey.0)));
let asset_wire = parsed.asset.unwrap_or(AssetWire::Native(
crate::server::types::NativeMarker::Native,
));
if let AssetWire::Contract { contract } = &asset_wire {
return handle_set_token_balance(state, account_id, &parsed.amount, contract).await;
}
let amount: i64 = parsed.amount.parse().map_err(|e| {
JsonRpcError::invalid_params(format!(
"amount: not a valid i64 decimal string ({e}): {:?}",
parsed.amount
))
})?;
if amount < 0 {
return Err(JsonRpcError::invalid_params(format!(
"amount: must be >= 0, got {amount}"
)));
}
let (lookup_key, trustline_asset_for_create) = match &asset_wire {
AssetWire::Native(_) => (
LedgerKey::Account(LedgerKeyAccount {
account_id: account_id.clone(),
}),
None,
),
AssetWire::Credit { code, issuer } => {
let issuer_strkey =
stellar_strkey::ed25519::PublicKey::from_string(issuer).map_err(|e| {
JsonRpcError::invalid_params(format!(
"asset.issuer: not a valid G... strkey: {e}"
))
})?;
let issuer_id = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256(issuer_strkey.0)));
let trustline_asset = match code.len() {
0 => {
return Err(JsonRpcError::invalid_params(
"asset.code: must be 1-12 chars, got empty",
));
}
1..=4 => {
let mut bytes = [0u8; 4];
bytes[..code.len()].copy_from_slice(code.as_bytes());
TrustLineAsset::CreditAlphanum4(AlphaNum4 {
asset_code: AssetCode4(bytes),
issuer: issuer_id,
})
}
5..=12 => {
let mut bytes = [0u8; 12];
bytes[..code.len()].copy_from_slice(code.as_bytes());
TrustLineAsset::CreditAlphanum12(AlphaNum12 {
asset_code: AssetCode12(bytes),
issuer: issuer_id,
})
}
len => {
return Err(JsonRpcError::invalid_params(format!(
"asset.code: must be 1-12 chars, got {len}"
)));
}
};
(
LedgerKey::Trustline(LedgerKeyTrustLine {
account_id: account_id.clone(),
asset: trustline_asset.clone(),
}),
Some(trustline_asset),
)
}
AssetWire::Contract { .. } => unreachable!("Contract dispatched separately above"),
};
let existing = state
.actor
.send(|tx| Command::GetLedgerEntries {
keys: vec![lookup_key.clone()],
reply: tx,
})
.await
.map_err(|e| JsonRpcError::internal_error(e.to_string()))?;
let existing_entry = existing.entries.into_iter().next().flatten();
let new_entry = match (asset_wire, existing_entry, trustline_asset_for_create) {
(AssetWire::Native(_), Some((_, mut entry, _)), _) => {
match &mut entry.data {
LedgerEntryData::Account(account) => account.balance = amount,
other => {
return Err(JsonRpcError::internal_error(format!(
"expected Account entry under Account key, got {other:?} \
(cache corruption or LedgerKey/LedgerEntry shape mismatch)"
)));
}
}
entry.last_modified_ledger_seq = existing.latest_ledger;
entry
}
(AssetWire::Native(_), None, _) => LedgerEntry {
last_modified_ledger_seq: existing.latest_ledger,
data: LedgerEntryData::Account(AccountEntry {
account_id: account_id.clone(),
balance: amount,
seq_num: SequenceNumber((existing.latest_ledger as i64) << 32),
num_sub_entries: 0,
inflation_dest: None,
flags: 0,
home_domain: Default::default(),
thresholds: Thresholds([1, 0, 0, 0]),
signers: Default::default(),
ext: AccountEntryExt::V0,
}),
ext: LedgerEntryExt::V0,
},
(AssetWire::Credit { .. }, Some((_, mut entry, _)), _) => {
match &mut entry.data {
LedgerEntryData::Trustline(tl) => tl.balance = amount,
other => {
return Err(JsonRpcError::internal_error(format!(
"expected Trustline entry under Trustline key, got {other:?} \
(cache corruption or LedgerKey/LedgerEntry shape mismatch)"
)));
}
}
entry.last_modified_ledger_seq = existing.latest_ledger;
entry
}
(AssetWire::Credit { .. }, None, Some(asset)) => LedgerEntry {
last_modified_ledger_seq: existing.latest_ledger,
data: LedgerEntryData::Trustline(TrustLineEntry {
account_id: account_id.clone(),
asset,
balance: amount,
limit: i64::MAX,
flags: 1, ext: TrustLineEntryExt::V0,
}),
ext: LedgerEntryExt::V0,
},
(AssetWire::Credit { .. }, None, None) => {
return Err(JsonRpcError::internal_error(
"internal: credit asset without trustline-asset construction (bug)",
));
}
(AssetWire::Contract { .. }, _, _) => {
unreachable!("Contract dispatched separately above")
}
};
state
.actor
.send(|tx| Command::SetLedgerEntry {
key: lookup_key,
entry: new_entry,
live_until: None,
reply: tx,
})
.await
.map_err(|e| JsonRpcError::internal_error(e.to_string()))?;
let body = SetBalanceResponse {
ok: true,
latest_ledger: existing.latest_ledger,
};
serde_json::to_value(body).map_err(|e| JsonRpcError::internal_error(e.to_string()))
}
async fn handle_set_token_balance(
state: &AppState,
to_account: soroban_env_host::xdr::AccountId,
amount_str: &str,
contract_strkey: &str,
) -> Result<serde_json::Value, JsonRpcError> {
use soroban_env_host::xdr::{
AccountId, ContractId, Hash, HostFunction, Int128Parts, InvokeContractArgs,
InvokeHostFunctionOp, Memo, MuxedAccount, Operation, OperationBody, Preconditions,
PublicKey, ScAddress, ScSymbol, ScVal, SequenceNumber, Transaction, TransactionEnvelope,
TransactionExt, TransactionV1Envelope, Uint256,
};
let target_amount: i128 = amount_str.parse().map_err(|e| {
JsonRpcError::invalid_params(format!(
"amount: not a valid i128 decimal string ({e}): {amount_str:?}"
))
})?;
if target_amount < 0 {
return Err(JsonRpcError::invalid_params(format!(
"amount: must be >= 0, got {target_amount}"
)));
}
let contract_parsed = stellar_strkey::Contract::from_string(contract_strkey).map_err(|e| {
JsonRpcError::invalid_params(format!("asset.contract: not a valid C... strkey: {e}"))
})?;
let contract_address = ScAddress::Contract(ContractId(Hash(contract_parsed.0)));
let to_address = ScAddress::Account(to_account.clone());
let zero_source = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0u8; 32])));
let balance_fn = HostFunction::InvokeContract(InvokeContractArgs {
contract_address: contract_address.clone(),
function_name: ScSymbol(
"balance"
.try_into()
.expect("'balance' fits in 32-char ScSymbol"),
),
args: vec![ScVal::Address(to_address.clone())]
.try_into()
.expect("single-arg vec into VecM"),
});
let sim_reply = state
.actor
.send(|tx| Command::SimulateTransaction {
host_function: balance_fn,
source_account: zero_source.clone(),
transaction_size_bytes: 0,
reply: tx,
})
.await
.map_err(|e| JsonRpcError::internal_error(e.to_string()))?;
let current_balance: i128 = match sim_reply.result {
Ok(ScVal::I128(Int128Parts { hi, lo })) => ((hi as i128) << 64) | (lo as i128),
Ok(ScVal::U64(_) | ScVal::U32(_) | ScVal::I32(_) | ScVal::I64(_)) => {
return Err(JsonRpcError::invalid_params(format!(
"asset.contract: token's balance() returned a non-i128 ScVal — \
not a SEP-41-shaped token. Got: {:?}",
sim_reply.result
)));
}
Ok(other) => {
return Err(JsonRpcError::invalid_params(format!(
"asset.contract: token's balance() returned unexpected ScVal type: {other:?}"
)));
}
Err(e) => {
return Err(JsonRpcError::invalid_params(format!(
"asset.contract: token's balance() simulation failed: {e}. \
Is the contract a SEP-41-shaped token? Does the account exist?"
)));
}
};
let delta = target_amount - current_balance;
if delta == 0 {
let body = SetBalanceResponse {
ok: true,
latest_ledger: sim_reply.latest_ledger,
};
return serde_json::to_value(body).map_err(|e| JsonRpcError::internal_error(e.to_string()));
}
let (fn_name, fn_args) = if delta > 0 {
let delta_scval = i128_to_scval(delta);
(
"mint",
vec![ScVal::Address(to_address.clone()), delta_scval],
)
} else {
let abs_delta = delta
.checked_abs()
.ok_or_else(|| JsonRpcError::invalid_params("amount: i128::MIN delta unreachable"))?;
let delta_scval = i128_to_scval(abs_delta);
("burn", vec![ScVal::Address(to_address), delta_scval])
};
let mutate_fn = HostFunction::InvokeContract(InvokeContractArgs {
contract_address,
function_name: ScSymbol(
fn_name
.try_into()
.expect("fn name fits in 32-char ScSymbol"),
),
args: fn_args.try_into().expect("two-arg vec into VecM"),
});
let op = Operation {
source_account: None,
body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
host_function: mutate_fn.clone(),
auth: vec![]
.try_into()
.expect("empty vec into VecM<SorobanAuthorizationEntry>"),
}),
};
let tx = Transaction {
source_account: MuxedAccount::Ed25519(Uint256([0u8; 32])),
fee: 0,
seq_num: SequenceNumber(0),
cond: Preconditions::None,
memo: Memo::None,
operations: vec![op].try_into().expect("single-op vec into VecM"),
ext: TransactionExt::V0,
};
let envelope = TransactionEnvelope::Tx(TransactionV1Envelope {
tx,
signatures: vec![].try_into().expect("empty signatures vec into VecM"),
});
let envelope_bytes = envelope
.to_xdr(soroban_env_host::xdr::Limits::none())
.map_err(|e| JsonRpcError::internal_error(format!("encode synthetic envelope: {e}")))?;
let send_reply = state
.actor
.send(|tx| Command::SendTransaction {
envelope_bytes,
host_function: mutate_fn,
source_account: zero_source,
reply: tx,
})
.await
.map_err(|e| JsonRpcError::internal_error(e.to_string()))?;
if let Err(msg) = &send_reply.receipt.result {
return Err(JsonRpcError::invalid_params(format!(
"token {fn_name}({delta}) failed: {msg}. Is the contract SEP-41-shaped \
with public mint/burn? Trust-mode auth bypasses admin checks but the \
function must still exist and accept (Address, i128)."
)));
}
let latest = state
.actor
.send(|tx| Command::GetLatestLedger { reply: tx })
.await
.map_err(|e| JsonRpcError::internal_error(e.to_string()))?;
let body = SetBalanceResponse {
ok: true,
latest_ledger: latest.sequence,
};
serde_json::to_value(body).map_err(|e| JsonRpcError::internal_error(e.to_string()))
}
fn i128_to_scval(n: i128) -> soroban_env_host::xdr::ScVal {
use soroban_env_host::xdr::{Int128Parts, ScVal};
let hi: i64 = (n >> 64) as i64;
let lo: u64 = (n & 0xFFFF_FFFF_FFFF_FFFF) as u64;
ScVal::I128(Int128Parts { hi, lo })
}
async fn handle_fork_etch(
state: &AppState,
params: &serde_json::Value,
) -> Result<serde_json::Value, JsonRpcError> {
use sha2::{Digest, Sha256};
use soroban_env_host::xdr::{
ContractCodeEntry, ContractCodeEntryExt, ContractDataDurability, ContractDataEntry,
ContractExecutable, ContractId, ExtensionPoint, Hash, LedgerEntry, LedgerEntryData,
LedgerEntryExt, LedgerKey, LedgerKeyContractCode, LedgerKeyContractData, ScAddress,
ScContractInstance, ScVal,
};
let parsed: EtchParams = serde_json::from_value(params.clone())
.map_err(|e| JsonRpcError::invalid_params(format!("fork_etch params: {e}")))?;
let contract_strkey = stellar_strkey::Contract::from_string(&parsed.contract).map_err(|e| {
JsonRpcError::invalid_params(format!("contract: not a valid C... strkey: {e}"))
})?;
let contract_address = ScAddress::Contract(ContractId(Hash(contract_strkey.0)));
let wasm = BASE64
.decode(&parsed.wasm)
.map_err(|e| JsonRpcError::invalid_params(format!("wasm: base64 decode: {e}")))?;
let new_hash_bytes: [u8; 32] = Sha256::digest(&wasm).into();
let new_hash = Hash(new_hash_bytes);
let code_key = LedgerKey::ContractCode(LedgerKeyContractCode {
hash: new_hash.clone(),
});
let code_entry = LedgerEntry {
last_modified_ledger_seq: 0,
data: LedgerEntryData::ContractCode(ContractCodeEntry {
ext: ContractCodeEntryExt::V0,
hash: new_hash.clone(),
code: wasm.try_into().map_err(|_| {
JsonRpcError::invalid_params("wasm: bytes exceed XDR BytesM<u32::MAX> capacity")
})?,
}),
ext: LedgerEntryExt::V0,
};
state
.actor
.send(|tx| Command::SetLedgerEntry {
key: code_key,
entry: code_entry,
live_until: parsed.live_until_ledger_seq,
reply: tx,
})
.await
.map_err(|e| JsonRpcError::internal_error(e.to_string()))?;
let instance_key = LedgerKey::ContractData(LedgerKeyContractData {
contract: contract_address.clone(),
key: ScVal::LedgerKeyContractInstance,
durability: ContractDataDurability::Persistent,
});
let existing = state
.actor
.send(|tx| Command::GetLedgerEntries {
keys: vec![instance_key.clone()],
reply: tx,
})
.await
.map_err(|e| JsonRpcError::internal_error(e.to_string()))?;
let existing_entry = existing.entries.into_iter().next().flatten();
let preserved_storage = match existing_entry {
Some((_, entry, _)) => match &entry.data {
LedgerEntryData::ContractData(cd) => match &cd.val {
ScVal::ContractInstance(instance) => instance.storage.clone(),
other => {
return Err(JsonRpcError::internal_error(format!(
"instance entry's val is not ContractInstance: {other:?} \
(host invariant violation; refusing to overwrite)"
)));
}
},
other => {
return Err(JsonRpcError::internal_error(format!(
"instance LedgerKey resolved to non-ContractData entry: {other:?}"
)));
}
},
None => None,
};
let new_instance_val = ScVal::ContractInstance(ScContractInstance {
executable: ContractExecutable::Wasm(new_hash),
storage: preserved_storage,
});
let new_instance_entry = LedgerEntry {
last_modified_ledger_seq: 0,
data: LedgerEntryData::ContractData(ContractDataEntry {
ext: ExtensionPoint::V0,
contract: contract_address,
key: ScVal::LedgerKeyContractInstance,
durability: ContractDataDurability::Persistent,
val: new_instance_val,
}),
ext: LedgerEntryExt::V0,
};
state
.actor
.send(|tx| Command::SetLedgerEntry {
key: instance_key,
entry: new_instance_entry,
live_until: parsed.live_until_ledger_seq,
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 = SetCodeResponse {
ok: true,
hash: hex_lower(&new_hash_bytes),
latest_ledger: latest.sequence,
};
serde_json::to_value(body).map_err(|e| JsonRpcError::internal_error(e.to_string()))
}
async fn handle_fork_close_ledgers(
state: &AppState,
params: &serde_json::Value,
) -> Result<serde_json::Value, JsonRpcError> {
let parsed: CloseLedgersParams = if params.is_null() {
CloseLedgersParams {
ledgers: None,
timestamp_advance_seconds: None,
}
} else {
serde_json::from_value(params.clone())
.map_err(|e| JsonRpcError::invalid_params(format!("fork_closeLedgers params: {e}")))?
};
let ledgers = parsed.ledgers.unwrap_or(1);
let timestamp_advance_seconds = parsed
.timestamp_advance_seconds
.unwrap_or(ledgers as u64 * 5);
let reply = state
.actor
.send(|tx| Command::CloseLedgers {
ledgers,
timestamp_advance_seconds,
reply: tx,
})
.await
.map_err(|e| JsonRpcError::internal_error(e.to_string()))?;
let body = CloseLedgersResponse {
new_sequence: reply.new_sequence,
new_close_time: reply.new_close_time.to_string(),
};
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,
}
}