diff --git a/Cargo.lock b/Cargo.lock index a93428a..9fec307 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3605,6 +3605,7 @@ dependencies = [ "indexmap 2.7.1", "itertools 0.14.0", "log 0.4.25", + "qualifier_attr", "solana-address-lookup-table-program", "solana-bpf-loader-program", "solana-compute-budget", diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 22513c8..e18f595 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -36,7 +36,7 @@ jsonrpc-core-client = "18.0.0" jsonrpc-http-server = "18.0.0" libc = "0.2.169" regex = "1.11.1" -litesvm = "0.5.0" +litesvm = { version = "0.5.0", features = ["nodejs-internal"] } crossbeam = "0.8.4" # litesvm = { path = "../../../litesvm/crates/litesvm" } diff --git a/crates/core/src/rpc/full.rs b/crates/core/src/rpc/full.rs index 1490ae5..b829dd9 100644 --- a/crates/core/src/rpc/full.rs +++ b/crates/core/src/rpc/full.rs @@ -1,11 +1,9 @@ -use std::str::FromStr; - -use super::utils::decode_and_deserialize; +use super::utils::{decode_and_deserialize, transform_tx_metadata_to_ui_accounts}; use jsonrpc_core::futures::future::{self, join_all}; use jsonrpc_core::BoxFuture; use jsonrpc_core::{Error, Result}; use jsonrpc_derive::rpc; -use litesvm::types::TransactionResult; +use solana_account_decoder::{encode_ui_account, UiAccountEncoding}; use solana_client::rpc_config::RpcContextConfig; use solana_client::rpc_custom_error::RpcCustomError; use solana_client::rpc_response::RpcApiVersion; @@ -22,16 +20,17 @@ use solana_client::{ }, }; use solana_rpc_client_api::response::Response as RpcResponse; -use solana_sdk::clock::UnixTimestamp; use solana_sdk::message::VersionedMessage; use solana_sdk::pubkey::Pubkey; use solana_sdk::signature::Signature; -use solana_sdk::transaction::VersionedTransaction; +use solana_sdk::transaction::{Transaction, VersionedTransaction}; +use solana_sdk::{account::Account, clock::UnixTimestamp}; use solana_transaction_status::{ EncodedConfirmedTransactionWithStatusMeta, TransactionConfirmationStatus, TransactionStatus, UiConfirmedBlock, }; use solana_transaction_status::{TransactionBinaryEncoding, UiTransactionEncoding}; +use std::str::FromStr; use super::*; @@ -94,7 +93,7 @@ pub trait Full { meta: Self::Metadata, data: String, config: Option, - ) -> Result>; + ) -> BoxFuture>>; #[rpc(meta, name = "minimumLedgerSlot")] fn minimum_ledger_slot(&self, meta: Self::Metadata) -> Result; @@ -325,15 +324,8 @@ impl Full for SurfpoolFullRpc { data: String, config: Option, ) -> Result { - let RpcSendTransactionConfig { - skip_preflight, - preflight_commitment, - encoding, - max_retries, - min_context_slot, - } = config.unwrap_or_default(); - - let tx_encoding = encoding.unwrap_or(UiTransactionEncoding::Base58); + let config = config.unwrap_or_default(); + let tx_encoding = config.encoding.unwrap_or(UiTransactionEncoding::Base58); let binary_encoding = tx_encoding.into_binary_encoding().ok_or_else(|| { Error::invalid_params(format!( "unsupported encoding: {tx_encoding}. Supported encodings: base58, base64" @@ -362,8 +354,134 @@ impl Full for SurfpoolFullRpc { meta: Self::Metadata, data: String, config: Option, - ) -> Result> { - unimplemented!() + ) -> BoxFuture>> { + let config = config.unwrap_or_default(); + let (_bytes, tx): (Vec<_>, Transaction) = match decode_and_deserialize( + data, + config + .encoding + .map(|enconding| enconding.into_binary_encoding()) + .flatten() + .unwrap_or(TransactionBinaryEncoding::Base58), + ) { + Ok(res) => res, + Err(e) => return Box::pin(future::err(e)), + }; + + let (local_accounts, replacement_blockhash, rpc_client) = { + let state = match meta.get_state_mut() { + Ok(res) => res, + Err(e) => return Box::pin(future::err(e.into())), + }; + let local_accounts: Vec> = tx + .message + .account_keys + .clone() + .into_iter() + .map(|pk| state.svm.get_account(&pk)) + .collect(); + let replacement_blockhash = Some(RpcBlockhash { + blockhash: state.svm.latest_blockhash().to_string(), + last_valid_block_height: state.epoch_info.block_height, + }); + let rpc_client = state.rpc_client.clone(); + + (local_accounts, replacement_blockhash, rpc_client) + }; + + Box::pin(async move { + let fetched_accounts = join_all( + tx.message + .account_keys + .iter() + .map(|pk| async { rpc_client.get_account(pk).await.ok() }), + ) + .await; + + let mut state_writer = meta.get_state_mut()?; + state_writer.svm.set_sigverify(config.sig_verify); + // // TODO: LiteSVM does not enable replacing the current blockhash + + // Update missing local accounts + tx.message + .account_keys + .iter() + .zip(local_accounts.iter().zip(fetched_accounts)) + .map(|(pk, (local, fetched))| { + if local.is_none() { + if let Some(account) = fetched { + state_writer.svm.set_account(*pk, account).map_err(|err| { + Error::invalid_params(format!( + "failed to save fetched account {pk:?}: {err:?}" + )) + })?; + } + } + Ok(()) + }) + .collect::>()?; + + match state_writer.svm.simulate_transaction(tx) { + Ok(tx_info) => Ok(RpcResponse { + context: RpcResponseContext::new(state_writer.epoch_info.absolute_slot), + value: RpcSimulateTransactionResult { + err: None, + logs: Some(tx_info.meta.logs.clone()), + accounts: if let Some(accounts) = config.accounts { + Some( + accounts + .addresses + .iter() + .map(|pk_str| { + if let Some((pk, account)) = tx_info + .post_accounts + .iter() + .find(|(pk, _)| pk.to_string() == *pk_str) + { + Some(encode_ui_account( + pk, + account, + UiAccountEncoding::Base64, + None, + None, + )) + } else { + None + } + }) + .collect(), + ) + } else { + None + }, + units_consumed: Some(tx_info.meta.compute_units_consumed), + return_data: Some(tx_info.meta.return_data.clone().into()), + inner_instructions: if config.inner_instructions { + Some(transform_tx_metadata_to_ui_accounts(&tx_info.meta)) + } else { + None + }, + replacement_blockhash, + }, + }), + Err(tx_info) => Ok(RpcResponse { + context: RpcResponseContext::new(state_writer.epoch_info.absolute_slot), + value: RpcSimulateTransactionResult { + err: Some(tx_info.err), + logs: Some(tx_info.meta.logs.clone()), + accounts: None, + units_consumed: Some(tx_info.meta.compute_units_consumed), + return_data: Some(tx_info.meta.return_data.clone().into()), + inner_instructions: if config.inner_instructions { + Some(transform_tx_metadata_to_ui_accounts(&tx_info.meta)) + } else { + None + }, + replacement_blockhash, + }, + }), + } + }) } fn minimum_ledger_slot(&self, meta: Self::Metadata) -> Result { diff --git a/crates/core/src/rpc/utils.rs b/crates/core/src/rpc/utils.rs index 33c7f4d..e381af7 100644 --- a/crates/core/src/rpc/utils.rs +++ b/crates/core/src/rpc/utils.rs @@ -3,6 +3,7 @@ use std::{any::type_name, sync::Arc}; use base64::prelude::*; use bincode::Options; use jsonrpc_core::{Error, Result}; +use litesvm::types::TransactionMetadata; use solana_client::{ rpc_config::RpcTokenAccountsFilter, rpc_custom_error::RpcCustomError, @@ -14,7 +15,9 @@ use solana_sdk::{ hash::Hash, packet::PACKET_DATA_SIZE, pubkey::Pubkey, signature::Signature, transaction::SanitizedTransaction, }; -use solana_transaction_status::TransactionBinaryEncoding; +use solana_transaction_status::{ + InnerInstruction, InnerInstructions, TransactionBinaryEncoding, UiInnerInstructions, +}; fn optimize_filters(filters: &mut [RpcFilterType]) { filters.iter_mut().for_each(|filter_type| { @@ -168,3 +171,25 @@ where }) .map(|output| (wire_output, output)) } + +pub fn transform_tx_metadata_to_ui_accounts( + meta: &TransactionMetadata, +) -> Vec { + meta.inner_instructions + .iter() + .enumerate() + .map(|(i, ixs)| { + InnerInstructions { + index: i as u8, + instructions: ixs + .iter() + .map(|ix| InnerInstruction { + instruction: ix.instruction.clone(), + stack_height: Some(ix.stack_height as u32), + }) + .collect(), + } + .into() + }) + .collect() +}