diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 22e8e4fdc1..4c7c26f308 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -25,7 +25,9 @@ use solana_sdk::{ timing::slot_duration_from_slots_per_year, transaction::{self, Transaction}, }; -use solana_transaction_status::{ConfirmedBlock, TransactionEncoding, TransactionStatus}; +use solana_transaction_status::{ + ConfirmedBlock, ConfirmedTransaction, TransactionEncoding, TransactionStatus, +}; use solana_vote_program::vote_state::{VoteState, MAX_LOCKOUT_HISTORY}; use std::{ collections::HashMap, @@ -506,6 +508,21 @@ impl JsonRpcRequestProcessor { } }) } + + pub fn get_confirmed_transaction( + &self, + signature: Signature, + encoding: Option, + ) -> Result> { + if self.config.enable_rpc_transaction_history { + Ok(self + .blockstore + .get_confirmed_transaction(signature, encoding) + .unwrap_or(None)) + } else { + Ok(None) + } + } } fn get_tpu_addr(cluster_info: &Arc>) -> Result { @@ -747,6 +764,14 @@ pub trait RpcSol { start_slot: Slot, end_slot: Option, ) -> Result>; + + #[rpc(meta, name = "getConfirmedTransaction")] + fn get_confirmed_transaction( + &self, + meta: Self::Metadata, + signature_str: String, + encoding: Option, + ) -> Result>; } pub struct RpcSolImpl; @@ -1290,6 +1315,19 @@ impl RpcSol for RpcSolImpl { fn get_block_time(&self, meta: Self::Metadata, slot: Slot) -> Result> { meta.request_processor.read().unwrap().get_block_time(slot) } + + fn get_confirmed_transaction( + &self, + meta: Self::Metadata, + signature_str: String, + encoding: Option, + ) -> Result> { + let signature = verify_signature(&signature_str)?; + meta.request_processor + .read() + .unwrap() + .get_confirmed_transaction(signature, encoding) + } } #[cfg(test)] diff --git a/docs/src/apps/jsonrpc-api.md b/docs/src/apps/jsonrpc-api.md index d47f736528..cd6a5fdc44 100644 --- a/docs/src/apps/jsonrpc-api.md +++ b/docs/src/apps/jsonrpc-api.md @@ -21,6 +21,7 @@ To interact with a Solana node inside a JavaScript application, use the [solana- * [getClusterNodes](jsonrpc-api.md#getclusternodes) * [getConfirmedBlock](jsonrpc-api.md#getconfirmedblock) * [getConfirmedBlocks](jsonrpc-api.md#getconfirmedblocks) +* [getConfirmedTransaction](jsonrpc-api.md#getconfirmedtransaction) * [getEpochInfo](jsonrpc-api.md#getepochinfo) * [getEpochSchedule](jsonrpc-api.md#getepochschedule) * [getFeeCalculatorForBlockhash](jsonrpc-api.md#getfeecalculatorforblockhash) @@ -345,6 +346,45 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0","id":1,"m {"jsonrpc":"2.0","result":[5,6,7,8,9,10],"id":1} ``` +### getConfirmedTransaction + +Returns transaction details for a confirmed transaction + +#### Parameters: + +* `` - transaction signature as base-58 encoded string +* `` - (optional) encoding for the returned Transaction, either "json" or "binary". If not provided, the default encoding is JSON. + +#### Results: + +The result field will be an object with the following fields: +* `slot: ` - the slot this transaction was processed in +* `transaction: ` - [Transaction](#transaction-structure) object, either in JSON format or base-58 encoded binary data, depending on encoding parameter +* `meta: ` - transaction status metadata object, containing `null` or: + * `err: ` - Error if transaction failed, null if transaction succeeded. [TransactionError definitions](https://github.com/solana-labs/solana/blob/master/sdk/src/transaction.rs#L14) + * `fee: ` - fee this transaction was charged, as u64 integer + * `preBalances: ` - array of u64 account balances from before the transaction was processed + * `postBalances: ` - array of u64 account balances after the transaction was processed + * DEPRECATED: `status: ` - Transaction status + * `"Ok": ` - Transaction was successful + * `"Err": ` - Transaction failed with TransactionError + +#### Example: + +```bash +// Request +curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0","id":1,"method":"getConfirmedTransaction","params":["35YGay1Lwjwgxe9zaH6APSHbt9gYQUCtBWTNL3aVwVGn9xTFw2fgds7qK5AL29mP63A9j3rh8KpN1TgSR62XCaby", "json"]}' localhost:8899 + +// Result +{"jsonrpc":"2.0","result":{"slot":430,"transaction":{"message":{"accountKeys":["6H94zdiaYfRfPfKjYLjyr2VFBg6JHXygy84r3qhc3NsC","39UAy8hsoYPywGPGdmun747omSr79zLSjqvPJN3zetoH","SysvarS1otHashes111111111111111111111111111","SysvarC1ock11111111111111111111111111111111","Vote111111111111111111111111111111111111111"],"header":{"numReadonlySignedAccounts":0,"numReadonlyUnsignedAccounts":3,"numRequiredSignatures":2},"instructions":[{"accounts":[1,2,3],"data":"29z5mr1JoRmJYQ6ynmk3pf31cGFRziAF1M3mT3L6sFXf5cKLdkEaMXMT8AqLpD4CpcupHmuMEmtZHpomrwfdZetSomNy3d","programIdIndex":4}],"recentBlockhash":"EFejToxii1L5aUF2NrK9dsbAEmZSNyN5nsipmZHQR1eA"},"signatures":["35YGay1Lwjwgxe9zaH6APSHbt9gYQUCtBWTNL3aVwVGn9xTFw2fgds7qK5AL29mP63A9j3rh8KpN1TgSR62XCaby","4vANMjSKiwEchGSXwVrQkwHnmsbKQmy9vdrsYxWdCup1bLsFzX8gKrFTSVDCZCae2dbxJB9mPNhqB2sD1vvr4sAD"]},"meta":{"err":null,"fee":18000,"postBalances":[499999972500,15298080,1,1,1],"preBalances":[499999990500,15298080,1,1,1],"status":{"Ok":null}}},"id":1} + +// Request +curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0","id":1,"method":"getConfirmedTransaction","params":["35YGay1Lwjwgxe9zaH6APSHbt9gYQUCtBWTNL3aVwVGn9xTFw2fgds7qK5AL29mP63A9j3rh8KpN1TgSR62XCaby", "binary"]}' localhost:8899 + +// Result +{"jsonrpc":"2.0","result":{"slot":430,"transaction":"81UZJt4dh4Do66jDhrgkQudS8J2N6iG3jaVav7gJrqJSFY4Ug53iA9JFJZh2gxKWcaFdLJwhHx9mRdg9JwDAWB4ywiu5154CRwXV4FMdnPLg7bhxRLwhhYaLsVgMF5AyNRcTzjCVoBvqFgDU7P8VEKDEiMvD3qxzm1pLZVxDG1LTQpT3Dz4Uviv4KQbFQNuC22KupBoyHFB7Zh6KFdMqux4M9PvhoqcoJsJKwXjWpKu7xmEKnnrSbfLadkgjBmmjhW3fdTrFvnhQdTkhtdJxUL1xS9GMuJQer8YgSKNtUXB1eXZQwXU8bU2BjYkZE6Q5Xww8hu9Z4E4Mo4QsooVtHoP6BM3NKw8zjVbWfoCQqxTrwuSzrNCWCWt58C24LHecH67CTt2uXbYSviixvrYkK7A3t68BxTJcF1dXJitEPTFe2ceTkauLJqrJgnER4iUrsjr26T8YgWvpY9wkkWFSviQW6wV5RASTCUasVEcrDiaKj8EQMkgyDoe9HyKitSVg67vMWJFpUXpQobseWJUs5FTWWzmfHmFp8FZ","meta":{"err":null,"fee":18000,"postBalances":[499999972500,15298080,1,1,1],"preBalances":[499999990500,15298080,1,1,1],"status":{"Ok":null}}},"id":1} +``` + ### getEpochInfo Returns information about the current epoch diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index f8f2311ba5..2203386897 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -37,8 +37,8 @@ use solana_sdk::{ transaction::Transaction, }; use solana_transaction_status::{ - ConfirmedBlock, EncodedTransaction, Rewards, RpcTransactionStatusMeta, TransactionEncoding, - TransactionStatusMeta, TransactionWithStatusMeta, + ConfirmedBlock, ConfirmedTransaction, EncodedTransaction, Rewards, RpcTransactionStatusMeta, + TransactionEncoding, TransactionStatusMeta, TransactionWithStatusMeta, }; use solana_vote_program::{vote_instruction::VoteInstruction, vote_state::TIMESTAMP_SLOT_INTERVAL}; use std::{ @@ -1721,6 +1721,42 @@ impl Blockstore { .map(|(status, _)| status) } + /// Returns a complete transaction if it was processed in a root + pub fn get_confirmed_transaction( + &self, + signature: Signature, + encoding: Option, + ) -> Result> { + if let Some((slot, status)) = self.get_transaction_status(signature.clone())? { + let transaction = self.find_transaction_in_slot(slot, signature)? + .expect("Transaction to exist in slot entries if it exists in statuses and hasn't been cleaned up"); + let encoding = encoding.unwrap_or(TransactionEncoding::Json); + let encoded_transaction = EncodedTransaction::encode(transaction, encoding); + Ok(Some(ConfirmedTransaction { + slot, + transaction: TransactionWithStatusMeta { + transaction: encoded_transaction, + meta: Some(status.into()), + }, + })) + } else { + Ok(None) + } + } + + fn find_transaction_in_slot( + &self, + slot: Slot, + signature: Signature, + ) -> Result> { + let slot_entries = self.get_slot_entries(slot, 0, None)?; + Ok(slot_entries + .iter() + .cloned() + .flat_map(|entry| entry.transactions) + .find(|transaction| transaction.signatures[0] == signature)) + } + pub fn read_rewards(&self, index: Slot) -> Result> { self.rewards_cf.get(index) } @@ -5911,6 +5947,88 @@ pub mod tests { Blockstore::destroy(&blockstore_path).expect("Expected successful database destruction"); } + #[test] + fn test_get_confirmed_transaction() { + let slot = 2; + let entries = make_slot_entries_with_transactions(5); + let shreds = entries_to_test_shreds(entries.clone(), slot, slot - 1, true, 0); + let ledger_path = get_tmp_ledger_path!(); + let blockstore = Blockstore::open(&ledger_path).unwrap(); + blockstore.insert_shreds(shreds, None, false).unwrap(); + blockstore.set_roots(&[slot - 1, slot]).unwrap(); + + let expected_transactions: Vec<(Transaction, Option)> = entries + .iter() + .cloned() + .filter(|entry| !entry.is_tick()) + .flat_map(|entry| entry.transactions) + .map(|transaction| { + let mut pre_balances: Vec = vec![]; + let mut post_balances: Vec = vec![]; + for (i, _account_key) in transaction.message.account_keys.iter().enumerate() { + pre_balances.push(i as u64 * 10); + post_balances.push(i as u64 * 11); + } + let signature = transaction.signatures[0]; + blockstore + .transaction_status_cf + .put( + (0, signature, slot), + &TransactionStatusMeta { + status: Ok(()), + fee: 42, + pre_balances: pre_balances.clone(), + post_balances: post_balances.clone(), + }, + ) + .unwrap(); + ( + transaction, + Some( + TransactionStatusMeta { + status: Ok(()), + fee: 42, + pre_balances, + post_balances, + } + .into(), + ), + ) + }) + .collect(); + + for (transaction, status) in expected_transactions.clone() { + let signature = transaction.signatures[0]; + let encoded_transaction = + EncodedTransaction::encode(transaction, TransactionEncoding::Json); + let expected_transaction = ConfirmedTransaction { + slot, + transaction: TransactionWithStatusMeta { + transaction: encoded_transaction, + meta: status, + }, + }; + assert_eq!( + blockstore + .get_confirmed_transaction(signature, None) + .unwrap(), + Some(expected_transaction) + ); + } + + blockstore.run_purge(0, 2).unwrap(); + *blockstore.lowest_cleanup_slot.write().unwrap() = slot; + for (transaction, _) in expected_transactions { + let signature = transaction.signatures[0]; + assert_eq!( + blockstore + .get_confirmed_transaction(signature, None) + .unwrap(), + None, + ); + } + } + #[test] fn test_get_last_hash() { let mut entries: Vec = vec![]; diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs index ce5c1ea32a..fd06614158 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -94,6 +94,14 @@ pub struct ConfirmedBlock { pub rewards: Rewards, } +#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConfirmedTransaction { + pub slot: Slot, + #[serde(flatten)] + pub transaction: TransactionWithStatusMeta, +} + /// A duplicate representation of a Transaction for pretty JSON serialization #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")]