use serde::{Deserialize, Serialize};
use validator::Validate;
use super::rpc_response_context::RpcResponseContext;
#[derive(Serialize, Deserialize, Debug)]
pub struct GetTransactionResponse {
#[serde(default)]
pub version: u16,
pub context: RpcResponseContext,
pub block_height: u64,
pub block_time: Option<i64>,
pub transaction: Transaction,
pub meta: TransactionStatusMetadata,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Transaction {
pub signatures: Vec<String>,
pub message: TransactionMessage,
pub valid_from: i64,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct TransactionMessage {
pub account_keys: Vec<String>,
pub header: MessageHeader,
pub instructions: Vec<Instruction>,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct MessageHeader {
pub num_required_signatures: u8,
pub num_readonly_signed_accounts: u8,
pub num_readonly_unsigned_accounts: u8,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Instruction {
pub accounts: Vec<u8>,
pub data: String,
pub program_id_index: u8,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct TransactionStatusMetadata {
pub err: Option<serde_json::Value>,
pub fee: u64,
pub log_messages: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub inner_instructions: Option<Vec<InnerInstructionEntry>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub compute_units_consumed: Option<u64>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct InnerInstructionEntry {
pub instruction_index: u8,
pub instruction: Instruction,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Validate)]
pub struct GetTransactionRequest {
#[serde(default)]
#[validate(custom(function = crate::validation::validate_protocol_version))]
pub version: u16,
#[validate(length(min = 1, message = "Signature cannot be empty"))]
#[validate(custom(function = crate::validation::validate_signature))]
pub signature: String,
}
impl GetTransactionRequest {
pub fn new(signature: String) -> Self {
Self {
version: 0,
signature,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_metadata() -> TransactionStatusMetadata {
TransactionStatusMetadata {
err: None,
fee: 5_000,
log_messages: Some(vec!["Program log: invoke".to_string()]),
inner_instructions: Some(vec![InnerInstructionEntry {
instruction_index: 0,
instruction: Instruction {
accounts: vec![0, 1],
data: "abc".to_string(),
program_id_index: 2,
},
}]),
compute_units_consumed: Some(1_234),
}
}
#[test]
fn transaction_status_metadata_serializes_as_camelcase() {
let meta = sample_metadata();
let value = serde_json::to_value(&meta).expect("serialize");
let obj = value.as_object().expect("object");
assert!(
obj.contains_key("logMessages"),
"missing logMessages: {obj:?}"
);
assert!(
obj.contains_key("innerInstructions"),
"missing innerInstructions: {obj:?}"
);
assert!(
obj.contains_key("computeUnitsConsumed"),
"missing computeUnitsConsumed: {obj:?}"
);
assert!(!obj.contains_key("log_messages"));
assert!(!obj.contains_key("inner_instructions"));
assert!(!obj.contains_key("compute_units_consumed"));
}
#[test]
fn transaction_status_metadata_survives_round_trip() {
let original = sample_metadata();
let value = serde_json::to_value(&original).expect("serialize");
let round_tripped: TransactionStatusMetadata =
serde_json::from_value(value).expect("deserialize");
assert_eq!(round_tripped.err, original.err);
assert_eq!(round_tripped.fee, original.fee);
assert_eq!(round_tripped.log_messages, original.log_messages);
assert_eq!(
round_tripped.compute_units_consumed,
original.compute_units_consumed
);
assert_eq!(
round_tripped.inner_instructions.as_ref().map(|v| v.len()),
original.inner_instructions.as_ref().map(|v| v.len())
);
}
#[test]
fn transaction_status_metadata_deserializes_camelcase_wire_format() {
let wire = serde_json::json!({
"err": null,
"fee": 5_000,
"logMessages": ["Program log: invoke"],
"innerInstructions": [],
"computeUnitsConsumed": 1_234
});
let meta: TransactionStatusMetadata =
serde_json::from_value(wire).expect("parse camelCase wire format");
assert_eq!(meta.fee, 5_000);
assert_eq!(
meta.log_messages.as_deref(),
Some(["Program log: invoke".to_string()].as_slice())
);
assert_eq!(meta.compute_units_consumed, Some(1_234));
assert_eq!(meta.inner_instructions.as_ref().map(|v| v.len()), Some(0));
}
#[test]
fn instruction_serializes_as_camelcase() {
let ix = Instruction {
accounts: vec![0, 1],
data: "abc".to_string(),
program_id_index: 2,
};
let value = serde_json::to_value(&ix).expect("serialize");
let obj = value.as_object().expect("object");
assert!(
obj.contains_key("programIdIndex"),
"Instruction must serialize program_id_index as programIdIndex to match \
the Solana JSON-RPC wire format and the CDK Value bridge reader at \
conversions.rs:142; got {obj:?}"
);
assert!(
!obj.contains_key("program_id_index"),
"snake_case key must not leak: {obj:?}"
);
}
#[test]
fn message_header_serializes_as_camelcase() {
let header = MessageHeader {
num_required_signatures: 1,
num_readonly_signed_accounts: 0,
num_readonly_unsigned_accounts: 1,
};
let value = serde_json::to_value(&header).expect("serialize");
let obj = value.as_object().expect("object");
assert!(obj.contains_key("numRequiredSignatures"), "{obj:?}");
assert!(obj.contains_key("numReadonlySignedAccounts"), "{obj:?}");
assert!(obj.contains_key("numReadonlyUnsignedAccounts"), "{obj:?}");
assert!(!obj.contains_key("num_required_signatures"));
assert!(!obj.contains_key("num_readonly_signed_accounts"));
assert!(!obj.contains_key("num_readonly_unsigned_accounts"));
}
#[test]
fn transaction_message_serializes_as_camelcase() {
let msg = TransactionMessage {
account_keys: vec!["k1".to_string(), "k2".to_string()],
header: MessageHeader {
num_required_signatures: 1,
num_readonly_signed_accounts: 0,
num_readonly_unsigned_accounts: 1,
},
instructions: vec![Instruction {
accounts: vec![0, 1],
data: "abc".to_string(),
program_id_index: 2,
}],
};
let value = serde_json::to_value(&msg).expect("serialize");
let obj = value.as_object().expect("object");
assert!(obj.contains_key("accountKeys"), "{obj:?}");
assert!(!obj.contains_key("account_keys"));
let instructions = obj
.get("instructions")
.and_then(|v| v.as_array())
.expect("instructions array");
let first = instructions
.first()
.and_then(|v| v.as_object())
.expect("first instruction object");
assert!(
first.contains_key("programIdIndex"),
"nested Instruction must still serialize as camelCase: {first:?}"
);
}
#[test]
fn transaction_serializes_valid_from_as_camelcase() {
let tx = Transaction {
signatures: vec!["sig".to_string()],
message: TransactionMessage {
account_keys: vec![],
header: MessageHeader {
num_required_signatures: 0,
num_readonly_signed_accounts: 0,
num_readonly_unsigned_accounts: 0,
},
instructions: vec![],
},
valid_from: 1_700_000_000,
};
let value = serde_json::to_value(&tx).expect("serialize");
let obj = value.as_object().expect("object");
assert!(
obj.contains_key("validFrom"),
"Transaction must serialize valid_from as validFrom to match the CDK \
Value bridge reader at conversions.rs:259; got {obj:?}"
);
assert!(
!obj.contains_key("valid_from"),
"snake_case key must not leak: {obj:?}"
);
}
#[test]
fn inner_instructions_deserialize_populated_camelcase_wire() {
let wire = serde_json::json!({
"err": null,
"fee": 5_000,
"logMessages": ["Program log: invoke"],
"innerInstructions": [{
"instructionIndex": 0,
"instruction": {
"accounts": [0, 1],
"data": "abc",
"programIdIndex": 2
}
}],
"computeUnitsConsumed": 1_234
});
let meta: TransactionStatusMetadata =
serde_json::from_value(wire).expect("parse populated camelCase wire format");
let inner = meta
.inner_instructions
.as_ref()
.expect("innerInstructions present");
assert_eq!(inner.len(), 1);
assert_eq!(inner[0].instruction_index, 0);
assert_eq!(inner[0].instruction.program_id_index, 2);
assert_eq!(inner[0].instruction.accounts, vec![0, 1]);
assert_eq!(inner[0].instruction.data, "abc");
}
}