v0.1.0-pre.026
This commit is contained in:
234
kb-onchain-transport/src/get_signatures_for_address.rs
Normal file
234
kb-onchain-transport/src/get_signatures_for_address.rs
Normal file
@@ -0,0 +1,234 @@
|
||||
// file: kb-onchain-transport/src/get_signatures_for_address.rs
|
||||
// version: 2
|
||||
|
||||
//! Standard Solana `getSignaturesForAddress` request and response contracts.
|
||||
|
||||
/// One transaction signature summary returned by `getSignaturesForAddress`.
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddressSignatureInfo {
|
||||
/// Transaction signature.
|
||||
pub signature: std::string::String,
|
||||
/// Slot containing the transaction.
|
||||
pub slot: u64,
|
||||
/// Optional transaction error returned by the RPC node.
|
||||
pub err: std::option::Option<serde_json::Value>,
|
||||
/// Optional memo associated with the transaction.
|
||||
pub memo: std::option::Option<std::string::String>,
|
||||
/// Optional block time as a Unix timestamp.
|
||||
pub block_time: std::option::Option<i64>,
|
||||
/// Optional confirmation status reported by the RPC node.
|
||||
pub confirmation_status: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
/// Configuration for one standard Solana `getSignaturesForAddress` request.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct GetSignaturesForAddressConfig {
|
||||
/// Requested commitment, restricted to `confirmed` or `finalized`.
|
||||
pub commitment: std::string::String,
|
||||
/// Maximum number of signatures requested from the node.
|
||||
pub limit: u16,
|
||||
/// Optional exclusive cursor toward older signatures.
|
||||
pub before: std::option::Option<std::string::String>,
|
||||
/// Optional exclusive lower boundary toward newer signatures.
|
||||
pub until: std::option::Option<std::string::String>,
|
||||
/// Optional minimum context slot.
|
||||
pub min_context_slot: std::option::Option<u64>,
|
||||
}
|
||||
|
||||
impl GetSignaturesForAddressConfig {
|
||||
/// Creates and validates one signature history request configuration.
|
||||
pub fn new(
|
||||
commitment: impl std::convert::Into<std::string::String>,
|
||||
limit: u16,
|
||||
before: std::option::Option<std::string::String>,
|
||||
until: std::option::Option<std::string::String>,
|
||||
min_context_slot: std::option::Option<u64>,
|
||||
) -> 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(
|
||||
"getSignaturesForAddress commitment must be 'confirmed' or 'finalized'",
|
||||
));
|
||||
}
|
||||
if limit == 0 || limit > 1000 {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"getSignaturesForAddress limit must be between 1 and 1000",
|
||||
));
|
||||
}
|
||||
let before_result = validate_optional_signature(
|
||||
before.as_ref(),
|
||||
"getSignaturesForAddress before signature",
|
||||
);
|
||||
if let std::result::Result::Err(error) = before_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let until_result =
|
||||
validate_optional_signature(until.as_ref(), "getSignaturesForAddress until signature");
|
||||
if let std::result::Result::Err(error) = until_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Ok(Self {
|
||||
commitment: commitment_value,
|
||||
limit,
|
||||
before,
|
||||
until,
|
||||
min_context_slot,
|
||||
});
|
||||
}
|
||||
|
||||
fn request_params(&self, address: &str) -> kb_core::Result<std::vec::Vec<serde_json::Value>> {
|
||||
let address_result =
|
||||
crate::validate_solana_pubkey_text(address, "getSignaturesForAddress address");
|
||||
if let std::result::Result::Err(error) = address_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let mut config = serde_json::Map::<std::string::String, serde_json::Value>::new();
|
||||
config.insert("commitment".to_string(), serde_json::Value::String(self.commitment.clone()));
|
||||
config.insert(
|
||||
"limit".to_string(),
|
||||
serde_json::Value::Number(serde_json::Number::from(self.limit)),
|
||||
);
|
||||
if let std::option::Option::Some(before) = &self.before {
|
||||
config.insert("before".to_string(), serde_json::Value::String(before.clone()));
|
||||
}
|
||||
if let std::option::Option::Some(until) = &self.until {
|
||||
config.insert("until".to_string(), serde_json::Value::String(until.clone()));
|
||||
}
|
||||
if let std::option::Option::Some(min_context_slot) = self.min_context_slot {
|
||||
config.insert(
|
||||
"minContextSlot".to_string(),
|
||||
serde_json::Value::Number(serde_json::Number::from(min_context_slot)),
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(std::vec![
|
||||
serde_json::Value::String(address.to_string()),
|
||||
serde_json::Value::Object(config),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::HttpClient {
|
||||
/// Fetches one page of signatures associated with an account or program address.
|
||||
pub async fn get_signatures_for_address(
|
||||
&self,
|
||||
address: &str,
|
||||
config: &crate::GetSignaturesForAddressConfig,
|
||||
) -> kb_core::Result<std::vec::Vec<crate::AddressSignatureInfo>> {
|
||||
let params_result = config.request_params(address);
|
||||
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("getSignaturesForAddress".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),
|
||||
};
|
||||
return crate::adapt_get_signatures_for_address_result(&value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Adapts a raw standard Solana `getSignaturesForAddress` result value.
|
||||
pub fn adapt_get_signatures_for_address_result(
|
||||
source: &serde_json::Value,
|
||||
) -> kb_core::Result<std::vec::Vec<crate::AddressSignatureInfo>> {
|
||||
let parse_result =
|
||||
serde_json::from_value::<std::vec::Vec<crate::AddressSignatureInfo>>(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 getSignaturesForAddress result: {error}"
|
||||
)));
|
||||
},
|
||||
};
|
||||
for item in &parsed {
|
||||
let validation_result = crate::validate_transaction_signature_text(
|
||||
item.signature.as_str(),
|
||||
"getSignaturesForAddress result signature",
|
||||
);
|
||||
if let std::result::Result::Err(error) = validation_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(parsed);
|
||||
}
|
||||
|
||||
fn validate_optional_signature(
|
||||
value: std::option::Option<&std::string::String>,
|
||||
field_name: &str,
|
||||
) -> kb_core::Result<()> {
|
||||
if let std::option::Option::Some(signature) = value {
|
||||
return crate::validate_transaction_signature_text(signature.as_str(), field_name);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn signature(byte: u8) -> std::string::String {
|
||||
return bs58::encode([byte; 64]).into_string();
|
||||
}
|
||||
|
||||
fn pubkey(byte: u8) -> std::string::String {
|
||||
return bs58::encode([byte; 32]).into_string();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_supports_before_and_until_for_bounded_pagination() {
|
||||
let config_result = crate::GetSignaturesForAddressConfig::new(
|
||||
"confirmed",
|
||||
1000,
|
||||
std::option::Option::Some(signature(1)),
|
||||
std::option::Option::Some(signature(2)),
|
||||
std::option::Option::Some(44),
|
||||
);
|
||||
let config = match config_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("unexpected config error: {error}"),
|
||||
};
|
||||
let params_result = config.request_params(pubkey(3).as_str());
|
||||
let params = match params_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("unexpected params error: {error}"),
|
||||
};
|
||||
assert_eq!(params.len(), 2);
|
||||
assert_eq!(params[1]["limit"], serde_json::json!(1000));
|
||||
assert_eq!(params[1]["minContextSlot"], serde_json::json!(44));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_rejects_limit_above_rpc_maximum() {
|
||||
let result = crate::GetSignaturesForAddressConfig::new(
|
||||
"confirmed",
|
||||
1001,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn result_adapter_preserves_signature_metadata() {
|
||||
let source = serde_json::json!([{
|
||||
"signature": signature(4),
|
||||
"slot": 123,
|
||||
"err": null,
|
||||
"memo": "hello",
|
||||
"blockTime": 456,
|
||||
"confirmationStatus": "finalized"
|
||||
}]);
|
||||
let result = crate::adapt_get_signatures_for_address_result(&source);
|
||||
let values = match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("unexpected adapter error: {error}"),
|
||||
};
|
||||
assert_eq!(values.len(), 1);
|
||||
assert_eq!(values[0].slot, 123);
|
||||
assert_eq!(values[0].memo.as_deref(), std::option::Option::Some("hello"));
|
||||
}
|
||||
}
|
||||
730
kb-onchain-transport/src/get_transaction.rs
Normal file
730
kb-onchain-transport/src/get_transaction.rs
Normal file
@@ -0,0 +1,730 @@
|
||||
// file: kb-onchain-transport/src/get_transaction.rs
|
||||
// version: 8
|
||||
|
||||
//! 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));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-onchain-transport/src/lib.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -11,6 +11,8 @@ mod client;
|
||||
mod constants;
|
||||
mod endpoint_role;
|
||||
mod execution_rpc;
|
||||
mod get_signatures_for_address;
|
||||
mod get_transaction;
|
||||
mod http_client;
|
||||
mod http_pool;
|
||||
mod json_rpc;
|
||||
@@ -124,6 +126,22 @@ pub use self::execution_rpc::adapt_send_transaction_result;
|
||||
pub use self::execution_rpc::adapt_simulate_transaction_result;
|
||||
/// Classifies a genesis hash as an official public cluster.
|
||||
pub use self::execution_rpc::classify_genesis_hash;
|
||||
/// One signature summary returned by `getSignaturesForAddress`.
|
||||
pub use self::get_signatures_for_address::AddressSignatureInfo;
|
||||
/// Configuration for `getSignaturesForAddress`.
|
||||
pub use self::get_signatures_for_address::GetSignaturesForAddressConfig;
|
||||
/// Adapts a raw `getSignaturesForAddress` result.
|
||||
pub use self::get_signatures_for_address::adapt_get_signatures_for_address_result;
|
||||
/// Common adapter contract for transport-specific transaction payloads.
|
||||
pub use self::get_transaction::CanonicalTransactionAdapter;
|
||||
/// Source payload and canonical result returned by `getTransaction`.
|
||||
pub use self::get_transaction::GetTransactionAcquisition;
|
||||
/// Standard Solana `getTransaction` adapter.
|
||||
pub use self::get_transaction::GetTransactionAdapter;
|
||||
/// Configuration for `getTransaction`.
|
||||
pub use self::get_transaction::GetTransactionConfig;
|
||||
/// Adapts a raw standard Solana `getTransaction` result.
|
||||
pub use self::get_transaction::adapt_get_transaction_result;
|
||||
/// HTTP JSON-RPC client bound to one endpoint.
|
||||
pub use self::http_client::HttpClient;
|
||||
/// HTTP method class used for routing diagnostics.
|
||||
|
||||
39
kb-onchain-transport/tests/fixtures/get_transaction_equivalent_a.json
vendored
Normal file
39
kb-onchain-transport/tests/fixtures/get_transaction_equivalent_a.json
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"slot": 104,
|
||||
"blockTime": null,
|
||||
"version": "legacy",
|
||||
"transaction": {
|
||||
"signatures": ["6psR5zDjAeh7HyLceXFQkWTJeRxjor8asYrG1AeUrrKVNXGE7DkvDqvVo98R9rPPQKHGm6TW3PBMSLrw2fHfxaX"],
|
||||
"message": {
|
||||
"header": {
|
||||
"numRequiredSignatures": 1,
|
||||
"numReadonlySignedAccounts": 0,
|
||||
"numReadonlyUnsignedAccounts": 1
|
||||
},
|
||||
"accountKeys": [
|
||||
"11111111111111111111111111111111",
|
||||
"ComputeBudget111111111111111111111111111111"
|
||||
],
|
||||
"recentBlockhash": "11111111111111111111111111111111",
|
||||
"instructions": [
|
||||
{"programIdIndex": 1, "accounts": [0], "data": "8", "stackHeight": null}
|
||||
],
|
||||
"addressTableLookups": []
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"err": null,
|
||||
"fee": 5000,
|
||||
"preBalances": [10000, 1],
|
||||
"postBalances": [5000, 1],
|
||||
"innerInstructions": null,
|
||||
"logMessages": null,
|
||||
"preTokenBalances": null,
|
||||
"postTokenBalances": null,
|
||||
"rewards": null,
|
||||
"loadedAddresses": null,
|
||||
"returnData": null,
|
||||
"computeUnitsConsumed": null,
|
||||
"costUnits": null
|
||||
}
|
||||
}
|
||||
30
kb-onchain-transport/tests/fixtures/get_transaction_equivalent_b.json
vendored
Normal file
30
kb-onchain-transport/tests/fixtures/get_transaction_equivalent_b.json
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"meta": {
|
||||
"postBalances": [5000, 1],
|
||||
"fee": 5000,
|
||||
"status": {"Ok": null},
|
||||
"err": null,
|
||||
"preBalances": [10000, 1]
|
||||
},
|
||||
"transaction": {
|
||||
"message": {
|
||||
"instructions": [
|
||||
{"data": "8", "accounts": [0], "programIdIndex": 1}
|
||||
],
|
||||
"recentBlockhash": "11111111111111111111111111111111",
|
||||
"accountKeys": [
|
||||
"11111111111111111111111111111111",
|
||||
"ComputeBudget111111111111111111111111111111"
|
||||
],
|
||||
"header": {
|
||||
"numReadonlyUnsignedAccounts": 1,
|
||||
"numReadonlySignedAccounts": 0,
|
||||
"numRequiredSignatures": 1
|
||||
}
|
||||
},
|
||||
"signatures": ["6psR5zDjAeh7HyLceXFQkWTJeRxjor8asYrG1AeUrrKVNXGE7DkvDqvVo98R9rPPQKHGm6TW3PBMSLrw2fHfxaX"]
|
||||
},
|
||||
"blockTime": null,
|
||||
"slot": 104,
|
||||
"providerSpecificField": "ignored"
|
||||
}
|
||||
37
kb-onchain-transport/tests/fixtures/get_transaction_legacy_failure.json
vendored
Normal file
37
kb-onchain-transport/tests/fixtures/get_transaction_legacy_failure.json
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"blockTime": 1700000002,
|
||||
"meta": {
|
||||
"err": {"InstructionError": [0, {"Custom": 6001}]},
|
||||
"fee": 5000,
|
||||
"innerInstructions": null,
|
||||
"loadedAddresses": {"readonly": [], "writable": []},
|
||||
"logMessages": ["Program 11111111111111111111111111111111 failed: custom program error: 0x1771"],
|
||||
"postBalances": [5000, 1],
|
||||
"postTokenBalances": [],
|
||||
"preBalances": [10000, 1],
|
||||
"preTokenBalances": [],
|
||||
"rewards": null,
|
||||
"status": {"Err": {"InstructionError": [0, {"Custom": 6001}]}}
|
||||
},
|
||||
"slot": 101,
|
||||
"transaction": {
|
||||
"message": {
|
||||
"accountKeys": [
|
||||
"11111111111111111111111111111111",
|
||||
"ComputeBudget111111111111111111111111111111"
|
||||
],
|
||||
"addressTableLookups": [],
|
||||
"header": {
|
||||
"numReadonlySignedAccounts": 0,
|
||||
"numReadonlyUnsignedAccounts": 1,
|
||||
"numRequiredSignatures": 1
|
||||
},
|
||||
"instructions": [
|
||||
{"accounts": [0], "data": "4", "programIdIndex": 1}
|
||||
],
|
||||
"recentBlockhash": "11111111111111111111111111111111"
|
||||
},
|
||||
"signatures": ["3LJnHMv3ygbULcerbSFVsk2Jo33Qv8DoyUJQh2E9UZAGYXcv7Y7KGZ3hPw5F5j9C9tBhVcYqipRnNEugKV1rnSU"]
|
||||
},
|
||||
"version": "legacy"
|
||||
}
|
||||
80
kb-onchain-transport/tests/fixtures/get_transaction_legacy_success.json
vendored
Normal file
80
kb-onchain-transport/tests/fixtures/get_transaction_legacy_success.json
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"slot": 100,
|
||||
"blockTime": 1700000001,
|
||||
"version": "legacy",
|
||||
"transaction": {
|
||||
"signatures": ["2Ana1pUpv2ZbMVkwF5FXapYeBEjdxDatLn7nvJkhgTSXbs59SyZSx866bXirPgj8QQVB57uxHJBG1YFvkRbFj4T"],
|
||||
"message": {
|
||||
"header": {
|
||||
"numRequiredSignatures": 1,
|
||||
"numReadonlySignedAccounts": 0,
|
||||
"numReadonlyUnsignedAccounts": 2
|
||||
},
|
||||
"accountKeys": [
|
||||
"11111111111111111111111111111111",
|
||||
"ComputeBudget111111111111111111111111111111",
|
||||
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
|
||||
],
|
||||
"recentBlockhash": "11111111111111111111111111111111",
|
||||
"instructions": [
|
||||
{
|
||||
"programIdIndex": 1,
|
||||
"accounts": [0],
|
||||
"data": "2",
|
||||
"stackHeight": 1
|
||||
}
|
||||
],
|
||||
"addressTableLookups": []
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"err": null,
|
||||
"status": {"Ok": null},
|
||||
"fee": 5000,
|
||||
"preBalances": [10000, 1, 1],
|
||||
"postBalances": [5000, 1, 1],
|
||||
"innerInstructions": [
|
||||
{
|
||||
"index": 0,
|
||||
"instructions": [
|
||||
{
|
||||
"programIdIndex": 2,
|
||||
"accounts": [0],
|
||||
"data": "3",
|
||||
"stackHeight": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"logMessages": [
|
||||
"Program ComputeBudget111111111111111111111111111111 invoke [1]",
|
||||
"Program ComputeBudget111111111111111111111111111111 success"
|
||||
],
|
||||
"preTokenBalances": [],
|
||||
"postTokenBalances": [
|
||||
{
|
||||
"accountIndex": 0,
|
||||
"mint": "So11111111111111111111111111111111111111112",
|
||||
"owner": "11111111111111111111111111111111",
|
||||
"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
|
||||
"uiTokenAmount": {
|
||||
"amount": "12345678901234567890",
|
||||
"decimals": 9,
|
||||
"uiAmount": null,
|
||||
"uiAmountString": "12345678901.23456789"
|
||||
}
|
||||
}
|
||||
],
|
||||
"rewards": [],
|
||||
"loadedAddresses": {
|
||||
"writable": [],
|
||||
"readonly": []
|
||||
},
|
||||
"returnData": {
|
||||
"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
|
||||
"data": ["AQI=", "base64"]
|
||||
},
|
||||
"computeUnitsConsumed": 1200,
|
||||
"costUnits": 1300
|
||||
}
|
||||
}
|
||||
28
kb-onchain-transport/tests/fixtures/get_transaction_optional_absent.json
vendored
Normal file
28
kb-onchain-transport/tests/fixtures/get_transaction_optional_absent.json
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"slot": 103,
|
||||
"blockTime": null,
|
||||
"transaction": {
|
||||
"signatures": ["5fMCpSnW6zfEJrShJAFSTaye2dexqwVfErfeETB34kbkRriTSfD3uQxtzjn2ToyKeqakLbpcbrvq5eDBTbs4uCW"],
|
||||
"message": {
|
||||
"header": {
|
||||
"numRequiredSignatures": 1,
|
||||
"numReadonlySignedAccounts": 0,
|
||||
"numReadonlyUnsignedAccounts": 1
|
||||
},
|
||||
"accountKeys": [
|
||||
"11111111111111111111111111111111",
|
||||
"ComputeBudget111111111111111111111111111111"
|
||||
],
|
||||
"recentBlockhash": "11111111111111111111111111111111",
|
||||
"instructions": [
|
||||
{"programIdIndex": 1, "accounts": [0], "data": "7"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"err": null,
|
||||
"fee": 5000,
|
||||
"preBalances": [10000, 1],
|
||||
"postBalances": [5000, 1]
|
||||
}
|
||||
}
|
||||
88
kb-onchain-transport/tests/fixtures/get_transaction_v0_alt_cpi_token_2022.json
vendored
Normal file
88
kb-onchain-transport/tests/fixtures/get_transaction_v0_alt_cpi_token_2022.json
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"slot": 102,
|
||||
"blockTime": 1700000003,
|
||||
"version": 0,
|
||||
"transaction": {
|
||||
"signatures": ["4VpzYuMH3LdMKjYmwoFUAfVyQqMBt2rjcAV2TjhbGet1VCAgn6fBaz1JCLRdmmZFuMtDv7BjALgJiwZRtYSTqpV"],
|
||||
"message": {
|
||||
"header": {
|
||||
"numRequiredSignatures": 1,
|
||||
"numReadonlySignedAccounts": 0,
|
||||
"numReadonlyUnsignedAccounts": 2
|
||||
},
|
||||
"accountKeys": [
|
||||
"11111111111111111111111111111111",
|
||||
"TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
|
||||
"ComputeBudget111111111111111111111111111111"
|
||||
],
|
||||
"recentBlockhash": "11111111111111111111111111111111",
|
||||
"instructions": [
|
||||
{
|
||||
"programIdIndex": 3,
|
||||
"accounts": [0, 4],
|
||||
"data": "5",
|
||||
"stackHeight": 1
|
||||
}
|
||||
],
|
||||
"addressTableLookups": [
|
||||
{
|
||||
"accountKey": "AddressLookupTab1e1111111111111111111111111",
|
||||
"writableIndexes": [0],
|
||||
"readonlyIndexes": [1]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"err": null,
|
||||
"fee": 7000,
|
||||
"preBalances": [100000, 1, 1, 1, 1],
|
||||
"postBalances": [93000, 1, 1, 1, 1],
|
||||
"innerInstructions": [
|
||||
{
|
||||
"index": 0,
|
||||
"instructions": [
|
||||
{
|
||||
"programIdIndex": 4,
|
||||
"accounts": [0, 1],
|
||||
"data": "6",
|
||||
"stackHeight": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"logMessages": ["Program log: Token-2022 CPI"],
|
||||
"preTokenBalances": [
|
||||
{
|
||||
"accountIndex": 1,
|
||||
"mint": "So11111111111111111111111111111111111111112",
|
||||
"owner": "11111111111111111111111111111111",
|
||||
"programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
|
||||
"uiTokenAmount": {
|
||||
"amount": "10",
|
||||
"decimals": 0,
|
||||
"uiAmountString": "10"
|
||||
}
|
||||
}
|
||||
],
|
||||
"postTokenBalances": [
|
||||
{
|
||||
"accountIndex": 1,
|
||||
"mint": "So11111111111111111111111111111111111111112",
|
||||
"owner": "11111111111111111111111111111111",
|
||||
"programId": "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb",
|
||||
"uiTokenAmount": {
|
||||
"amount": "9",
|
||||
"decimals": 0,
|
||||
"uiAmountString": "9"
|
||||
}
|
||||
}
|
||||
],
|
||||
"rewards": [],
|
||||
"loadedAddresses": {
|
||||
"writable": ["SysvarRent111111111111111111111111111111111"],
|
||||
"readonly": ["SysvarC1ock11111111111111111111111111111111"]
|
||||
},
|
||||
"computeUnitsConsumed": 9999
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user