parent
5e43304eca
commit
427c78d891
@ -119,6 +119,19 @@ impl RpcClient {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn simulate_transaction(
|
||||
&self,
|
||||
transaction: &Transaction,
|
||||
sig_verify: bool,
|
||||
) -> RpcResult<TransactionStatus> {
|
||||
let serialized_encoded = bs58::encode(serialize(transaction).unwrap()).into_string();
|
||||
self.send(
|
||||
RpcRequest::SimulateTransaction,
|
||||
json!([serialized_encoded, { "sigVerify": sig_verify }]),
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_signature_status(
|
||||
&self,
|
||||
signature: &Signature,
|
||||
|
@ -6,6 +6,12 @@ pub struct RpcSignatureStatusConfig {
|
||||
pub search_transaction_history: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RpcSimulateTransactionConfig {
|
||||
pub sig_verify: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum RpcLargestAccountsFilter {
|
||||
|
@ -40,6 +40,7 @@ pub enum RpcRequest {
|
||||
RegisterNode,
|
||||
RequestAirdrop,
|
||||
SendTransaction,
|
||||
SimulateTransaction,
|
||||
SignVote,
|
||||
GetMinimumBalanceForRentExemption,
|
||||
MinimumLedgerSlot,
|
||||
@ -84,6 +85,7 @@ impl fmt::Display for RpcRequest {
|
||||
RpcRequest::RegisterNode => "registerNode",
|
||||
RpcRequest::RequestAirdrop => "requestAirdrop",
|
||||
RpcRequest::SendTransaction => "sendTransaction",
|
||||
RpcRequest::SimulateTransaction => "simulateTransaction",
|
||||
RpcRequest::SignVote => "signVote",
|
||||
RpcRequest::GetMinimumBalanceForRentExemption => "getMinimumBalanceForRentExemption",
|
||||
RpcRequest::MinimumLedgerSlot => "minimumLedgerSlot",
|
||||
|
221
core/src/rpc.rs
221
core/src/rpc.rs
@ -847,6 +847,14 @@ pub trait RpcSol {
|
||||
#[rpc(meta, name = "sendTransaction")]
|
||||
fn send_transaction(&self, meta: Self::Metadata, data: String) -> Result<String>;
|
||||
|
||||
#[rpc(meta, name = "simulateTransaction")]
|
||||
fn simulate_transaction(
|
||||
&self,
|
||||
meta: Self::Metadata,
|
||||
data: String,
|
||||
config: Option<RpcSimulateTransactionConfig>,
|
||||
) -> RpcResponse<TransactionStatus>;
|
||||
|
||||
#[rpc(meta, name = "getSlotLeader")]
|
||||
fn get_slot_leader(
|
||||
&self,
|
||||
@ -1327,41 +1335,67 @@ impl RpcSol for RpcSolImpl {
|
||||
}
|
||||
|
||||
fn send_transaction(&self, meta: Self::Metadata, data: String) -> Result<String> {
|
||||
let data = bs58::decode(data).into_vec().unwrap();
|
||||
if data.len() >= PACKET_DATA_SIZE {
|
||||
info!(
|
||||
"send_transaction: transaction too large: {} bytes (max: {} bytes)",
|
||||
data.len(),
|
||||
PACKET_DATA_SIZE
|
||||
);
|
||||
return Err(Error::invalid_request());
|
||||
}
|
||||
let tx: Transaction = bincode::config()
|
||||
.limit(PACKET_DATA_SIZE as u64)
|
||||
.deserialize(&data)
|
||||
.map_err(|err| {
|
||||
info!("send_transaction: deserialize error: {:?}", err);
|
||||
Error::invalid_request()
|
||||
})?;
|
||||
|
||||
let (wire_transaction, transaction) = deserialize_bs58_transaction(data)?;
|
||||
let transactions_socket = UdpSocket::bind("0.0.0.0:0").unwrap();
|
||||
let tpu_addr = get_tpu_addr(&meta.cluster_info)?;
|
||||
trace!("send_transaction: leader is {:?}", &tpu_addr);
|
||||
transactions_socket
|
||||
.send_to(&data, tpu_addr)
|
||||
.send_to(&wire_transaction, tpu_addr)
|
||||
.map_err(|err| {
|
||||
info!("send_transaction: send_to error: {:?}", err);
|
||||
Error::internal_error()
|
||||
})?;
|
||||
let signature = tx.signatures[0].to_string();
|
||||
let signature = transaction.signatures[0].to_string();
|
||||
trace!(
|
||||
"send_transaction: sent {} bytes, signature={}",
|
||||
data.len(),
|
||||
wire_transaction.len(),
|
||||
signature
|
||||
);
|
||||
Ok(signature)
|
||||
}
|
||||
|
||||
fn simulate_transaction(
|
||||
&self,
|
||||
meta: Self::Metadata,
|
||||
data: String,
|
||||
config: Option<RpcSimulateTransactionConfig>,
|
||||
) -> RpcResponse<TransactionStatus> {
|
||||
let (_, transaction) = deserialize_bs58_transaction(data)?;
|
||||
let config = config.unwrap_or(RpcSimulateTransactionConfig { sig_verify: false });
|
||||
|
||||
let bank = &*meta.request_processor.read().unwrap().bank(None)?;
|
||||
assert!(bank.is_frozen());
|
||||
|
||||
let mut result = if config.sig_verify {
|
||||
transaction.verify()
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
|
||||
if result.is_ok() {
|
||||
let transactions = [transaction];
|
||||
let batch = bank.prepare_batch(&transactions, None);
|
||||
let (
|
||||
_loaded_accounts,
|
||||
executed,
|
||||
_retryable_transactions,
|
||||
_transaction_count,
|
||||
_signature_count,
|
||||
) = bank.load_and_execute_transactions(&batch, solana_sdk::clock::MAX_PROCESSING_AGE);
|
||||
result = executed[0].0.clone();
|
||||
}
|
||||
|
||||
new_response(
|
||||
&bank,
|
||||
TransactionStatus {
|
||||
slot: bank.slot(),
|
||||
confirmations: Some(0),
|
||||
status: result.clone(),
|
||||
err: result.err(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn get_slot_leader(
|
||||
&self,
|
||||
meta: Self::Metadata,
|
||||
@ -1498,6 +1532,26 @@ impl RpcSol for RpcSolImpl {
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_bs58_transaction(bs58_transaction: String) -> Result<(Vec<u8>, Transaction)> {
|
||||
let wire_transaction = bs58::decode(bs58_transaction).into_vec().unwrap();
|
||||
if wire_transaction.len() >= PACKET_DATA_SIZE {
|
||||
info!(
|
||||
"transaction too large: {} bytes (max: {} bytes)",
|
||||
wire_transaction.len(),
|
||||
PACKET_DATA_SIZE
|
||||
);
|
||||
return Err(Error::invalid_request());
|
||||
}
|
||||
bincode::config()
|
||||
.limit(PACKET_DATA_SIZE as u64)
|
||||
.deserialize(&wire_transaction)
|
||||
.map_err(|err| {
|
||||
info!("transaction deserialize error: {:?}", err);
|
||||
Error::invalid_request()
|
||||
})
|
||||
.map(|transaction| (wire_transaction, transaction))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
@ -2146,6 +2200,133 @@ pub mod tests {
|
||||
assert_eq!(expected, result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rpc_simulate_transaction() {
|
||||
let bob_pubkey = Pubkey::new_rand();
|
||||
let RpcHandler {
|
||||
io,
|
||||
meta,
|
||||
blockhash,
|
||||
alice,
|
||||
bank,
|
||||
..
|
||||
} = start_rpc_handler_with_tx(&bob_pubkey);
|
||||
|
||||
let mut tx = system_transaction::transfer(&alice, &bob_pubkey, 1234, blockhash);
|
||||
let tx_serialized_encoded = bs58::encode(serialize(&tx).unwrap()).into_string();
|
||||
tx.signatures[0] = Signature::default();
|
||||
let tx_badsig_serialized_encoded = bs58::encode(serialize(&tx).unwrap()).into_string();
|
||||
|
||||
bank.freeze(); // Ensure the root bank is frozen, `start_rpc_handler_with_tx()` doesn't do this
|
||||
|
||||
// Good signature with sigVerify=true
|
||||
let req = format!(
|
||||
r#"{{"jsonrpc":"2.0","id":1,"method":"simulateTransaction","params":["{}", {{"sigVerify": true}}]}}"#,
|
||||
tx_serialized_encoded,
|
||||
);
|
||||
let res = io.handle_request_sync(&req, meta.clone());
|
||||
let expected = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"context":{"slot":0},
|
||||
"value":{"confirmations":0,"slot": 0,"status":{"Ok":null},"err":null}
|
||||
},
|
||||
"id": 1,
|
||||
});
|
||||
let expected: Response =
|
||||
serde_json::from_value(expected).expect("expected response deserialization");
|
||||
let result: Response = serde_json::from_str(&res.expect("actual response"))
|
||||
.expect("actual response deserialization");
|
||||
assert_eq!(expected, result);
|
||||
|
||||
// Bad signature with sigVerify=true
|
||||
let req = format!(
|
||||
r#"{{"jsonrpc":"2.0","id":1,"method":"simulateTransaction","params":["{}", {{"sigVerify": true}}]}}"#,
|
||||
tx_badsig_serialized_encoded,
|
||||
);
|
||||
let res = io.handle_request_sync(&req, meta.clone());
|
||||
let expected = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"context":{"slot":0},
|
||||
"value":{"confirmations":0,"slot":0,"status":{"Err":"SignatureFailure"},"err":"SignatureFailure"}
|
||||
},
|
||||
"id": 1,
|
||||
});
|
||||
let expected: Response =
|
||||
serde_json::from_value(expected).expect("expected response deserialization");
|
||||
let result: Response = serde_json::from_str(&res.expect("actual response"))
|
||||
.expect("actual response deserialization");
|
||||
assert_eq!(expected, result);
|
||||
|
||||
// Bad signature with sigVerify=false
|
||||
let req = format!(
|
||||
r#"{{"jsonrpc":"2.0","id":1,"method":"simulateTransaction","params":["{}", {{"sigVerify": false}}]}}"#,
|
||||
tx_serialized_encoded,
|
||||
);
|
||||
let res = io.handle_request_sync(&req, meta.clone());
|
||||
let expected = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"context":{"slot":0},
|
||||
"value":{"confirmations":0,"slot": 0,"status":{"Ok":null},"err":null}
|
||||
},
|
||||
"id": 1,
|
||||
});
|
||||
let expected: Response =
|
||||
serde_json::from_value(expected).expect("expected response deserialization");
|
||||
let result: Response = serde_json::from_str(&res.expect("actual response"))
|
||||
.expect("actual response deserialization");
|
||||
assert_eq!(expected, result);
|
||||
|
||||
// Bad signature with default sigVerify setting (false)
|
||||
let req = format!(
|
||||
r#"{{"jsonrpc":"2.0","id":1,"method":"simulateTransaction","params":["{}"]}}"#,
|
||||
tx_serialized_encoded,
|
||||
);
|
||||
let res = io.handle_request_sync(&req, meta.clone());
|
||||
let expected = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"result": {
|
||||
"context":{"slot":0},
|
||||
"value":{"confirmations":0,"slot": 0,"status":{"Ok":null},"err":null}
|
||||
},
|
||||
"id": 1,
|
||||
});
|
||||
let expected: Response =
|
||||
serde_json::from_value(expected).expect("expected response deserialization");
|
||||
let result: Response = serde_json::from_str(&res.expect("actual response"))
|
||||
.expect("actual response deserialization");
|
||||
assert_eq!(expected, result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_rpc_simulate_transaction_panic_on_unfrozen_bank() {
|
||||
let bob_pubkey = Pubkey::new_rand();
|
||||
let RpcHandler {
|
||||
io,
|
||||
meta,
|
||||
blockhash,
|
||||
alice,
|
||||
bank,
|
||||
..
|
||||
} = start_rpc_handler_with_tx(&bob_pubkey);
|
||||
|
||||
let tx = system_transaction::transfer(&alice, &bob_pubkey, 1234, blockhash);
|
||||
let tx_serialized_encoded = bs58::encode(serialize(&tx).unwrap()).into_string();
|
||||
|
||||
assert!(!bank.is_frozen());
|
||||
|
||||
let req = format!(
|
||||
r#"{{"jsonrpc":"2.0","id":1,"method":"simulateTransaction","params":["{}", {{"sigVerify": true}}]}}"#,
|
||||
tx_serialized_encoded,
|
||||
);
|
||||
|
||||
// should panic because `bank` is not frozen
|
||||
let _ = io.handle_request_sync(&req, meta.clone());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rpc_confirm_tx() {
|
||||
let bob_pubkey = Pubkey::new_rand();
|
||||
|
@ -50,6 +50,7 @@ To interact with a Solana node inside a JavaScript application, use the [solana-
|
||||
* [minimumLedgerSlot](jsonrpc-api.md#minimumledgerslot)
|
||||
* [requestAirdrop](jsonrpc-api.md#requestairdrop)
|
||||
* [sendTransaction](jsonrpc-api.md#sendtransaction)
|
||||
* [simulateTransaction](jsonrpc-api.md#simulatetransaction)
|
||||
* [setLogFilter](jsonrpc-api.md#setlogfilter)
|
||||
* [validatorExit](jsonrpc-api.md#validatorexit)
|
||||
* [Subscription Websocket](jsonrpc-api.md#subscription-websocket)
|
||||
@ -1112,10 +1113,34 @@ Creates new transaction
|
||||
|
||||
```bash
|
||||
// Request
|
||||
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"sendTransaction", "params":["3gKEMTuxvm3DKEJc4UyiyoNz1sxwdVRW2pyDDXqaCvUjGApnsazGh2y4W92zuaSSdJhBbWLYAkZokBt4N5oW27R7zCVaLLpLxvATL2GgheEh9DmmDR1P9r1ZqirVXM2fF3z5cafmc4EtwWc1UErFdCWj1qYvy4bDGMLXRYLURxaKytEEqrxz6JXj8rUHhDpjTZeFxmC6iAW3hZr6cmaAzewQCQfiEv2HfydriwHDtN95u3Y1EF6SuXxcRqox2aTjGye2Ln9zFj4XbnAtjCmkZhR"]}' http://localhost:8899
|
||||
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"sendTransaction", "params":["4hXTCkRzt9WyecNzV1XPgCDfGAZzQKNxLXgynz5QDuWWPSAZBZSHptvWRL3BjCvzUXRdKvHL2b7yGrRQcWyaqsaBCncVG7BFggS8w9snUts67BSh3EqKpXLUm5UMHfD7ZBe9GhARjbNQMLJ1QD3Spr6oMTBU6EhdB4RD8CP2xUxr2u3d6fos36PD98XS6oX8TQjLpsMwncs5DAMiD4nNnR8NBfyghGCWvCVifVwvA8B8TJxE1aiyiv2L429BCWfyzAme5sZW8rDb14NeCQHhZbtNqfXhcp2tAnaAT"]}' http://localhost:8899
|
||||
|
||||
// Result
|
||||
{"jsonrpc":"2.0","result":"2EBVM6cB8vAAD93Ktr6Vd8p67XPbQzCJX47MpReuiCXJAtcjaxpvWpcg9Ege1Nr5Tk3a2GFrByT7WPBjdsTycY9b","id":1}
|
||||
{"jsonrpc":"2.0","result":"2id3YC2jK9G5Wo2phDx4gJVAew8DcY5NAojnVuao8rkxwPYPe8cSwE5GzhEgJA2y8fVjDEo6iR6ykBvDxrTQrtpb","id":1}
|
||||
```
|
||||
|
||||
### simulateTransaction
|
||||
|
||||
Simulate sending a transaction
|
||||
|
||||
#### Parameters:
|
||||
|
||||
* `<string>` - Transaction, as base-58 encoded string. The transaction must have a valid blockhash, but is not required to be signed.
|
||||
* `<object>` - (optional) Configuration object containing the following field:
|
||||
* `sigVerify: <bool>` - if true the transaction signatures will be verified (default: false)
|
||||
|
||||
#### Results:
|
||||
|
||||
An RpcResponse containing a TransactionStatus object
|
||||
|
||||
#### Example:
|
||||
|
||||
```bash
|
||||
// Request
|
||||
curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"simulateTransaction", "params":["4hXTCkRzt9WyecNzV1XPgCDfGAZzQKNxLXgynz5QDuWWPSAZBZSHptvWRL3BjCvzUXRdKvHL2b7yGrRQcWyaqsaBCncVG7BFggS8w9snUts67BSh3EqKpXLUm5UMHfD7ZBe9GhARjbNQMLJ1QD3Spr6oMTBU6EhdB4RD8CP2xUxr2u3d6fos36PD98XS6oX8TQjLpsMwncs5DAMiD4nNnR8NBfyghGCWvCVifVwvA8B8TJxE1aiyiv2L429BCWfyzAme5sZW8rDb14NeCQHhZbtNqfXhcp2tAnaAT"]}' http://localhost:8899
|
||||
|
||||
// Result
|
||||
{"jsonrpc":"2.0","result":{"context":{"slot":218},"value":{"confirmations":0,"err":null,"slot":218,"status":{"Ok":null}}},"id":1}
|
||||
```
|
||||
|
||||
### setLogFilter
|
||||
|
Loading…
x
Reference in New Issue
Block a user