731 lines
28 KiB
Rust
731 lines
28 KiB
Rust
// file: kb-onchain-transport/src/get_transaction.rs
|
|
// version: 10
|
|
|
|
//! Standard Solana `getTransaction` adapter for the canonical transaction contract.
|
|
|
|
use base64::Engine; // rust-rules: trait-import
|
|
|
|
/// Common adapter contract for transport-specific transaction payloads.
|
|
pub trait CanonicalTransactionAdapter {
|
|
/// Source payload type accepted by the adapter.
|
|
type Source;
|
|
|
|
/// Adapts one source payload into an optional canonical transaction.
|
|
fn adapt(
|
|
&self,
|
|
source: &Self::Source,
|
|
) -> kb_core::Result<std::option::Option<kb_lib::MdCanonicalTransaction>>;
|
|
}
|
|
|
|
/// Configuration for a standard Solana `getTransaction` request.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct GetTransactionConfig {
|
|
/// Requested commitment, restricted to `confirmed` or `finalized`.
|
|
pub commitment: std::string::String,
|
|
/// Maximum supported numbered transaction version.
|
|
pub max_supported_transaction_version: u8,
|
|
}
|
|
|
|
impl GetTransactionConfig {
|
|
/// Creates and validates a `getTransaction` request configuration.
|
|
pub fn new(
|
|
commitment: impl std::convert::Into<std::string::String>,
|
|
max_supported_transaction_version: u8,
|
|
) -> kb_core::Result<Self> {
|
|
let commitment_value = commitment.into();
|
|
if commitment_value != "confirmed" && commitment_value != "finalized" {
|
|
return std::result::Result::Err(kb_core::Error::config(
|
|
"getTransaction commitment must be 'confirmed' or 'finalized'",
|
|
));
|
|
}
|
|
return std::result::Result::Ok(Self {
|
|
commitment: commitment_value,
|
|
max_supported_transaction_version,
|
|
});
|
|
}
|
|
|
|
/// Creates the default confirmed configuration supporting transaction version zero.
|
|
pub fn confirmed_v0() -> Self {
|
|
return Self {
|
|
commitment: "confirmed".to_string(),
|
|
max_supported_transaction_version: 0,
|
|
};
|
|
}
|
|
|
|
fn request_params(&self, signature: &str) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
|
|
let signature_result =
|
|
crate::validate_transaction_signature_text(signature, "getTransaction signature");
|
|
if let std::result::Result::Err(error) = signature_result {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
return std::result::Result::Ok(std::vec![
|
|
serde_json::Value::String(signature.to_string()),
|
|
serde_json::json!({
|
|
"commitment": self.commitment.clone(),
|
|
"encoding": "json",
|
|
"maxSupportedTransactionVersion": self.max_supported_transaction_version
|
|
}),
|
|
]);
|
|
}
|
|
}
|
|
|
|
/// Source payload and canonical result returned by one `getTransaction` request.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct GetTransactionAcquisition {
|
|
/// Source-specific JSON result retained only for immediate metrics and hashing.
|
|
pub source_json: serde_json::Value,
|
|
/// Canonical source-independent transaction when the RPC result is not null.
|
|
pub canonical_transaction: std::option::Option<kb_lib::MdCanonicalTransaction>,
|
|
}
|
|
|
|
/// Adapter from the standard Solana `getTransaction` JSON result to the canonical model.
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
pub struct GetTransactionAdapter;
|
|
|
|
impl crate::CanonicalTransactionAdapter for crate::GetTransactionAdapter {
|
|
type Source = serde_json::Value;
|
|
|
|
fn adapt(
|
|
&self,
|
|
source: &Self::Source,
|
|
) -> kb_core::Result<std::option::Option<kb_lib::MdCanonicalTransaction>> {
|
|
return adapt_get_transaction_result(source);
|
|
}
|
|
}
|
|
|
|
impl crate::HttpClient {
|
|
/// Fetches one source payload and its canonical transaction adaptation.
|
|
pub async fn get_transaction_acquisition(
|
|
&self,
|
|
signature: &str,
|
|
config: &crate::GetTransactionConfig,
|
|
) -> kb_core::Result<crate::GetTransactionAcquisition> {
|
|
let params_result = config.request_params(signature);
|
|
let params = match params_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let result = self.execute_json_rpc_result_raw("getTransaction".to_string(), params).await;
|
|
let value = match result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let adapter = crate::GetTransactionAdapter;
|
|
let canonical_result = crate::CanonicalTransactionAdapter::adapt(&adapter, &value);
|
|
let canonical_transaction = match canonical_result {
|
|
std::result::Result::Ok(canonical) => canonical,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return std::result::Result::Ok(crate::GetTransactionAcquisition {
|
|
source_json: value,
|
|
canonical_transaction,
|
|
});
|
|
}
|
|
|
|
/// Fetches and adapts one standard Solana transaction into the canonical model.
|
|
pub async fn get_transaction_canonical(
|
|
&self,
|
|
signature: &str,
|
|
config: &crate::GetTransactionConfig,
|
|
) -> kb_core::Result<std::option::Option<kb_lib::MdCanonicalTransaction>> {
|
|
let acquisition_result = self.get_transaction_acquisition(signature, config).await;
|
|
let acquisition = match acquisition_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return std::result::Result::Ok(acquisition.canonical_transaction);
|
|
}
|
|
}
|
|
|
|
/// Adapts a raw standard Solana `getTransaction` result value.
|
|
pub fn adapt_get_transaction_result(
|
|
source: &serde_json::Value,
|
|
) -> kb_core::Result<std::option::Option<kb_lib::MdCanonicalTransaction>> {
|
|
if source.is_null() {
|
|
return std::result::Result::Ok(std::option::Option::None);
|
|
}
|
|
let parse_result = serde_json::from_value::<RpcGetTransactionResult>(source.clone());
|
|
let parsed = match parse_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(kb_core::Error::json(format!(
|
|
"cannot parse getTransaction result: {error}"
|
|
)));
|
|
},
|
|
};
|
|
let canonical_result = adapt_parsed_transaction(parsed);
|
|
return match canonical_result {
|
|
std::result::Result::Ok(transaction) => {
|
|
std::result::Result::Ok(std::option::Option::Some(transaction))
|
|
},
|
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
|
};
|
|
}
|
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct RpcGetTransactionResult {
|
|
slot: u64,
|
|
#[serde(default)]
|
|
block_time: std::option::Option<i64>,
|
|
#[serde(default)]
|
|
meta: std::option::Option<RpcTransactionMeta>,
|
|
transaction: RpcJsonTransaction,
|
|
#[serde(default)]
|
|
version: std::option::Option<RpcTransactionVersion>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
#[serde(untagged)]
|
|
enum RpcTransactionVersion {
|
|
Legacy(std::string::String),
|
|
Number(u8),
|
|
}
|
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
struct RpcJsonTransaction {
|
|
signatures: std::vec::Vec<std::string::String>,
|
|
message: RpcRawMessage,
|
|
}
|
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct RpcRawMessage {
|
|
header: RpcMessageHeader,
|
|
account_keys: std::vec::Vec<std::string::String>,
|
|
recent_blockhash: std::string::String,
|
|
instructions: std::vec::Vec<RpcCompiledInstruction>,
|
|
#[serde(default)]
|
|
address_table_lookups: std::vec::Vec<RpcAddressTableLookup>,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, serde::Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct RpcMessageHeader {
|
|
num_required_signatures: u8,
|
|
num_readonly_signed_accounts: u8,
|
|
num_readonly_unsigned_accounts: u8,
|
|
}
|
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct RpcCompiledInstruction {
|
|
program_id_index: u16,
|
|
accounts: std::vec::Vec<u16>,
|
|
data: std::string::String,
|
|
#[serde(default)]
|
|
stack_height: std::option::Option<u32>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct RpcAddressTableLookup {
|
|
account_key: std::string::String,
|
|
writable_indexes: std::vec::Vec<u8>,
|
|
readonly_indexes: std::vec::Vec<u8>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct RpcTransactionMeta {
|
|
#[serde(default)]
|
|
err: std::option::Option<serde_json::Value>,
|
|
fee: u64,
|
|
pre_balances: std::vec::Vec<u64>,
|
|
post_balances: std::vec::Vec<u64>,
|
|
#[serde(default)]
|
|
inner_instructions: std::option::Option<std::vec::Vec<RpcInnerInstructionGroup>>,
|
|
#[serde(default)]
|
|
log_messages: std::option::Option<std::vec::Vec<std::string::String>>,
|
|
#[serde(default)]
|
|
pre_token_balances: std::option::Option<std::vec::Vec<RpcTokenBalance>>,
|
|
#[serde(default)]
|
|
post_token_balances: std::option::Option<std::vec::Vec<RpcTokenBalance>>,
|
|
#[serde(default)]
|
|
rewards: std::option::Option<std::vec::Vec<RpcReward>>,
|
|
#[serde(default)]
|
|
loaded_addresses: std::option::Option<RpcLoadedAddresses>,
|
|
#[serde(default)]
|
|
return_data: std::option::Option<RpcReturnData>,
|
|
#[serde(default)]
|
|
compute_units_consumed: std::option::Option<u64>,
|
|
#[serde(default)]
|
|
cost_units: std::option::Option<u64>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
struct RpcInnerInstructionGroup {
|
|
index: u16,
|
|
instructions: std::vec::Vec<RpcCompiledInstruction>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct RpcTokenBalance {
|
|
account_index: u16,
|
|
mint: std::string::String,
|
|
#[serde(default)]
|
|
owner: std::option::Option<std::string::String>,
|
|
#[serde(default)]
|
|
program_id: std::option::Option<std::string::String>,
|
|
ui_token_amount: RpcUiTokenAmount,
|
|
}
|
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct RpcUiTokenAmount {
|
|
amount: std::string::String,
|
|
decimals: u8,
|
|
}
|
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct RpcReward {
|
|
pubkey: std::string::String,
|
|
lamports: i64,
|
|
post_balance: u64,
|
|
#[serde(default)]
|
|
reward_type: std::option::Option<std::string::String>,
|
|
#[serde(default)]
|
|
commission: std::option::Option<u8>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, serde::Deserialize)]
|
|
struct RpcLoadedAddresses {
|
|
#[serde(default)]
|
|
writable: std::vec::Vec<std::string::String>,
|
|
#[serde(default)]
|
|
readonly: std::vec::Vec<std::string::String>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, serde::Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct RpcReturnData {
|
|
program_id: std::string::String,
|
|
data: std::vec::Vec<std::string::String>,
|
|
}
|
|
|
|
fn adapt_parsed_transaction(
|
|
parsed: RpcGetTransactionResult,
|
|
) -> kb_core::Result<kb_lib::MdCanonicalTransaction> {
|
|
let primary_signature = match parsed.transaction.signatures.first() {
|
|
std::option::Option::Some(value) => value.clone(),
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(kb_core::Error::json(
|
|
"getTransaction result contains no signatures",
|
|
));
|
|
},
|
|
};
|
|
let version_result = adapt_version(parsed.version);
|
|
let version = match version_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let loaded_addresses = match &parsed.meta {
|
|
std::option::Option::Some(metadata) => {
|
|
adapt_loaded_addresses(metadata.loaded_addresses.clone())
|
|
},
|
|
std::option::Option::None => kb_lib::MdCanonicalLoadedAddresses::default(),
|
|
};
|
|
let instructions_result = adapt_instructions(parsed.transaction.message.instructions);
|
|
let instructions = match instructions_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let metadata_result = adapt_metadata(parsed.meta);
|
|
let metadata = match metadata_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let transaction = kb_lib::MdCanonicalTransaction {
|
|
format_version: kb_lib::MD_CANONICAL_TRANSACTION_FORMAT_VERSION,
|
|
primary_signature,
|
|
slot: parsed.slot,
|
|
block_time: parsed.block_time,
|
|
version,
|
|
signatures: parsed.transaction.signatures,
|
|
message: kb_lib::MdCanonicalTransactionMessage {
|
|
header: kb_lib::MdCanonicalMessageHeader {
|
|
num_required_signatures: parsed.transaction.message.header.num_required_signatures,
|
|
num_readonly_signed_accounts: parsed
|
|
.transaction
|
|
.message
|
|
.header
|
|
.num_readonly_signed_accounts,
|
|
num_readonly_unsigned_accounts: parsed
|
|
.transaction
|
|
.message
|
|
.header
|
|
.num_readonly_unsigned_accounts,
|
|
},
|
|
static_account_keys: parsed.transaction.message.account_keys,
|
|
recent_blockhash: parsed.transaction.message.recent_blockhash,
|
|
instructions,
|
|
address_table_lookups: parsed
|
|
.transaction
|
|
.message
|
|
.address_table_lookups
|
|
.into_iter()
|
|
.map(|lookup| {
|
|
return kb_lib::MdCanonicalAddressTableLookup {
|
|
account_key: lookup.account_key,
|
|
writable_indexes: lookup.writable_indexes,
|
|
readonly_indexes: lookup.readonly_indexes,
|
|
};
|
|
})
|
|
.collect(),
|
|
loaded_addresses,
|
|
},
|
|
metadata,
|
|
};
|
|
let validation_result = transaction.validate();
|
|
if let std::result::Result::Err(error) = validation_result {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
return std::result::Result::Ok(transaction);
|
|
}
|
|
|
|
fn adapt_version(
|
|
version: std::option::Option<RpcTransactionVersion>,
|
|
) -> kb_core::Result<kb_lib::MdCanonicalTransactionVersion> {
|
|
return match version {
|
|
std::option::Option::Some(RpcTransactionVersion::Legacy(value)) => {
|
|
if value != "legacy" {
|
|
return std::result::Result::Err(kb_core::Error::json(format!(
|
|
"unsupported textual transaction version: {value}"
|
|
)));
|
|
}
|
|
std::result::Result::Ok(kb_lib::MdCanonicalTransactionVersion::Legacy)
|
|
},
|
|
std::option::Option::Some(RpcTransactionVersion::Number(value)) => {
|
|
std::result::Result::Ok(kb_lib::MdCanonicalTransactionVersion::Number(value))
|
|
},
|
|
std::option::Option::None => {
|
|
std::result::Result::Ok(kb_lib::MdCanonicalTransactionVersion::Legacy)
|
|
},
|
|
};
|
|
}
|
|
|
|
fn adapt_instructions(
|
|
instructions: std::vec::Vec<RpcCompiledInstruction>,
|
|
) -> kb_core::Result<std::vec::Vec<kb_lib::MdCanonicalCompiledInstruction>> {
|
|
let mut adapted = std::vec::Vec::with_capacity(instructions.len());
|
|
for instruction in instructions {
|
|
let instruction_result = adapt_instruction(instruction);
|
|
let canonical = match instruction_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
adapted.push(canonical);
|
|
}
|
|
return std::result::Result::Ok(adapted);
|
|
}
|
|
|
|
fn adapt_instruction(
|
|
instruction: RpcCompiledInstruction,
|
|
) -> kb_core::Result<kb_lib::MdCanonicalCompiledInstruction> {
|
|
let decode_result = bs58::decode(instruction.data.as_str()).into_vec();
|
|
let bytes = match decode_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(kb_core::Error::json(format!(
|
|
"getTransaction instruction data is not valid base58: {error}"
|
|
)));
|
|
},
|
|
};
|
|
let data_base64 = base64::engine::general_purpose::STANDARD.encode(bytes);
|
|
return std::result::Result::Ok(kb_lib::MdCanonicalCompiledInstruction {
|
|
program_id_index: instruction.program_id_index,
|
|
account_indexes: instruction.accounts,
|
|
data_base64,
|
|
stack_height: instruction.stack_height,
|
|
});
|
|
}
|
|
|
|
fn adapt_loaded_addresses(
|
|
loaded_addresses: std::option::Option<RpcLoadedAddresses>,
|
|
) -> kb_lib::MdCanonicalLoadedAddresses {
|
|
return match loaded_addresses {
|
|
std::option::Option::Some(value) => kb_lib::MdCanonicalLoadedAddresses {
|
|
writable: value.writable,
|
|
readonly: value.readonly,
|
|
},
|
|
std::option::Option::None => kb_lib::MdCanonicalLoadedAddresses::default(),
|
|
};
|
|
}
|
|
|
|
fn adapt_metadata(
|
|
metadata: std::option::Option<RpcTransactionMeta>,
|
|
) -> kb_core::Result<std::option::Option<kb_lib::MdCanonicalTransactionMetadata>> {
|
|
let source = match metadata {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
|
|
};
|
|
let inner_result = adapt_inner_instructions(source.inner_instructions.unwrap_or_default());
|
|
let inner_instructions = match inner_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let return_result = adapt_return_data(source.return_data);
|
|
let return_data = match return_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let pre_token_result = adapt_token_balances(source.pre_token_balances.unwrap_or_default());
|
|
let pre_token_balances = match pre_token_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let post_token_result = adapt_token_balances(source.post_token_balances.unwrap_or_default());
|
|
let post_token_balances = match post_token_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let status = if source.err.is_some() {
|
|
kb_lib::MdCanonicalTransactionStatus::Failed
|
|
} else {
|
|
kb_lib::MdCanonicalTransactionStatus::Success
|
|
};
|
|
return std::result::Result::Ok(std::option::Option::Some(
|
|
kb_lib::MdCanonicalTransactionMetadata {
|
|
status,
|
|
error: source.err,
|
|
fee: source.fee,
|
|
pre_balances: source.pre_balances,
|
|
post_balances: source.post_balances,
|
|
inner_instructions,
|
|
log_messages: source.log_messages.unwrap_or_default(),
|
|
pre_token_balances,
|
|
post_token_balances,
|
|
rewards: source
|
|
.rewards
|
|
.unwrap_or_default()
|
|
.into_iter()
|
|
.map(|reward| {
|
|
return kb_lib::MdCanonicalReward {
|
|
pubkey: reward.pubkey,
|
|
lamports: reward.lamports,
|
|
post_balance: reward.post_balance,
|
|
reward_type: reward.reward_type,
|
|
commission: reward.commission,
|
|
};
|
|
})
|
|
.collect(),
|
|
return_data,
|
|
compute_units_consumed: source.compute_units_consumed,
|
|
cost_units: source.cost_units,
|
|
},
|
|
));
|
|
}
|
|
|
|
fn adapt_inner_instructions(
|
|
groups: std::vec::Vec<RpcInnerInstructionGroup>,
|
|
) -> kb_core::Result<std::vec::Vec<kb_lib::MdCanonicalInnerInstructionGroup>> {
|
|
let mut adapted = std::vec::Vec::with_capacity(groups.len());
|
|
for group in groups {
|
|
let instructions_result = adapt_instructions(group.instructions);
|
|
let instructions = match instructions_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
adapted.push(kb_lib::MdCanonicalInnerInstructionGroup {
|
|
parent_instruction_index: group.index,
|
|
instructions,
|
|
});
|
|
}
|
|
return std::result::Result::Ok(adapted);
|
|
}
|
|
|
|
fn adapt_token_balances(
|
|
balances: std::vec::Vec<RpcTokenBalance>,
|
|
) -> kb_core::Result<std::vec::Vec<kb_lib::MdCanonicalTokenBalance>> {
|
|
let mut adapted = std::vec::Vec::with_capacity(balances.len());
|
|
for balance in balances {
|
|
let balance_result = kb_lib::MdCanonicalTokenBalance::new(
|
|
balance.account_index,
|
|
balance.mint,
|
|
balance.owner,
|
|
balance.program_id,
|
|
balance.ui_token_amount.amount,
|
|
balance.ui_token_amount.decimals,
|
|
);
|
|
let canonical = match balance_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
adapted.push(canonical);
|
|
}
|
|
return std::result::Result::Ok(adapted);
|
|
}
|
|
|
|
fn adapt_return_data(
|
|
return_data: std::option::Option<RpcReturnData>,
|
|
) -> kb_core::Result<std::option::Option<kb_lib::MdCanonicalReturnData>> {
|
|
let source = match return_data {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
|
|
};
|
|
if source.data.len() != 2 {
|
|
return std::result::Result::Err(kb_core::Error::json(
|
|
"getTransaction return data must contain payload and encoding",
|
|
));
|
|
}
|
|
if source.data[1] != "base64" {
|
|
return std::result::Result::Err(kb_core::Error::json(format!(
|
|
"unsupported getTransaction return data encoding: {}",
|
|
source.data[1]
|
|
)));
|
|
}
|
|
return std::result::Result::Ok(std::option::Option::Some(kb_lib::MdCanonicalReturnData {
|
|
program_id: source.program_id,
|
|
data_base64: source.data[0].clone(),
|
|
}));
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
fn fixture(name: &str) -> serde_json::Value {
|
|
let text = match name {
|
|
"legacy_success" => {
|
|
include_str!("../tests/fixtures/get_transaction_legacy_success.json")
|
|
},
|
|
"legacy_failure" => {
|
|
include_str!("../tests/fixtures/get_transaction_legacy_failure.json")
|
|
},
|
|
"v0_alt" => {
|
|
include_str!("../tests/fixtures/get_transaction_v0_alt_cpi_token_2022.json")
|
|
},
|
|
"optional_absent" => {
|
|
include_str!("../tests/fixtures/get_transaction_optional_absent.json")
|
|
},
|
|
"equivalent_a" => include_str!("../tests/fixtures/get_transaction_equivalent_a.json"),
|
|
"equivalent_b" => include_str!("../tests/fixtures/get_transaction_equivalent_b.json"),
|
|
_ => panic!("unknown fixture: {name}"),
|
|
};
|
|
let parse_result = serde_json::from_str(text);
|
|
match parse_result {
|
|
std::result::Result::Ok(value) => return value,
|
|
std::result::Result::Err(error) => panic!("fixture parse failed: {error}"),
|
|
}
|
|
}
|
|
|
|
fn adapt_fixture(name: &str) -> kb_lib::MdCanonicalTransaction {
|
|
let value = fixture(name);
|
|
let result = crate::adapt_get_transaction_result(&value);
|
|
match result {
|
|
std::result::Result::Ok(std::option::Option::Some(transaction)) => return transaction,
|
|
std::result::Result::Ok(std::option::Option::None) => {
|
|
panic!("fixture unexpectedly adapted to none");
|
|
},
|
|
std::result::Result::Err(error) => panic!("fixture adaptation failed: {error}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn null_result_adapts_to_none() {
|
|
let result = crate::adapt_get_transaction_result(&serde_json::Value::Null);
|
|
assert_eq!(result, std::result::Result::Ok(std::option::Option::None));
|
|
}
|
|
|
|
#[test]
|
|
fn legacy_success_preserves_token_balances_and_return_data() {
|
|
let transaction = adapt_fixture("legacy_success");
|
|
assert_eq!(transaction.version, kb_lib::MdCanonicalTransactionVersion::Legacy);
|
|
assert_eq!(transaction.block_time, std::option::Option::Some(1_700_000_001));
|
|
let metadata = match transaction.metadata {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => panic!("metadata missing"),
|
|
};
|
|
assert_eq!(metadata.status, kb_lib::MdCanonicalTransactionStatus::Success);
|
|
assert_eq!(transaction.message.instructions[0].data_base64, "AQ==");
|
|
assert_eq!(metadata.post_token_balances.len(), 1);
|
|
assert_eq!(metadata.post_token_balances[0].amount, "12345678901234567890");
|
|
assert_eq!(metadata.post_token_balances[0].decimal_amount, "12345678901.234567890");
|
|
assert!(metadata.return_data.is_some());
|
|
assert_eq!(metadata.inner_instructions.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn legacy_failure_preserves_error() {
|
|
let transaction = adapt_fixture("legacy_failure");
|
|
let metadata = match transaction.metadata {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => panic!("metadata missing"),
|
|
};
|
|
assert_eq!(metadata.status, kb_lib::MdCanonicalTransactionStatus::Failed);
|
|
assert!(metadata.error.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn version_zero_preserves_alt_cpi_and_token_2022() {
|
|
let transaction = adapt_fixture("v0_alt");
|
|
assert_eq!(transaction.version, kb_lib::MdCanonicalTransactionVersion::Number(0));
|
|
assert_eq!(transaction.message.address_table_lookups.len(), 1);
|
|
assert_eq!(transaction.message.loaded_addresses.writable.len(), 1);
|
|
let metadata = match transaction.metadata {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => panic!("metadata missing"),
|
|
};
|
|
assert_eq!(metadata.inner_instructions.len(), 1);
|
|
assert_eq!(
|
|
metadata.post_token_balances[0].program_id.as_deref(),
|
|
std::option::Option::Some("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn optional_fields_and_block_time_can_be_absent() {
|
|
let transaction = adapt_fixture("optional_absent");
|
|
assert_eq!(transaction.block_time, std::option::Option::None);
|
|
let metadata = match transaction.metadata {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => panic!("metadata missing"),
|
|
};
|
|
assert!(metadata.log_messages.is_empty());
|
|
assert!(metadata.rewards.is_empty());
|
|
assert!(metadata.return_data.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn equivalent_provider_shapes_produce_identical_hashes() {
|
|
let first = adapt_fixture("equivalent_a");
|
|
let second = adapt_fixture("equivalent_b");
|
|
let first_hash = match first.canonical_json_hash() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("first hash failed: {error}"),
|
|
};
|
|
let second_hash = match second.canonical_json_hash() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("second hash failed: {error}"),
|
|
};
|
|
assert_eq!(first_hash, second_hash);
|
|
}
|
|
|
|
#[test]
|
|
fn request_config_rejects_public_key_length_signature() {
|
|
let config = crate::GetTransactionConfig::confirmed_v0();
|
|
let result = config.request_params("3Bxs4NN8M2Yn4TLb7gR6Xy7n2D1Q8VjQqWcnpX8C6pQw");
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn request_config_rejects_processed_commitment() {
|
|
let result = crate::GetTransactionConfig::new("processed", 0);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn request_config_uses_json_encoding_and_version_zero() {
|
|
let config = crate::GetTransactionConfig::confirmed_v0();
|
|
let params_result = config.request_params(
|
|
"2Ana1pUpv2ZbMVkwF5FXapYeBEjdxDatLn7nvJkhgTSXbs59SyZSx866bXirPgj8QQVB57uxHJBG1YFvkRbFj4T",
|
|
);
|
|
let params = match params_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("request params failed: {error}"),
|
|
};
|
|
assert_eq!(params[1]["encoding"], serde_json::Value::String("json".to_string()));
|
|
assert_eq!(params[1]["maxSupportedTransactionVersion"], serde_json::Value::from(0));
|
|
}
|
|
}
|