Files
khadhroony-solana-project/crates/ksp-raw-transaction-lib/src/canonical.rs
2026-09-06 17:15:51 +02:00

326 lines
14 KiB
Rust

// file: crates/ksp-raw-transaction-lib/src/canonical.rs
// version: 2
use sha2::Digest; // rust-rules: trait-import
/// KSP-owned source-independent RAW transaction format identifier.
pub const RAW_TRANSACTION_FORMAT_ID: &str = "ksp.solana.raw_transaction";
/// Frozen KSP-owned RAW transaction format version.
pub const RAW_TRANSACTION_FORMAT_VERSION: u32 = 1;
/// Source-wire presence state preserving omitted, explicit null and concrete values independently.
#[non_exhaustive]
pub enum RawTransactionWireField<T> {
/// The source field was omitted entirely.
Omitted,
/// The source field was explicitly present as JSON/protocol null.
Null,
/// The source field carried one concrete value.
Value(T),
}
impl<T> std::fmt::Debug for crate::RawTransactionWireField<T> {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return match self {
Self::Omitted => formatter.write_str("Omitted"),
Self::Null => formatter.write_str("Null"),
Self::Value(_) => formatter.write_str("Value(..)"),
};
}
}
/// Canonical Solana transaction version admitted by RAW transaction format v1.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RawTransactionVersion {
/// Legacy unversioned transaction representation.
Legacy,
/// Explicit versioned transaction number admitted by the current Solana wire contract.
Number(u8),
}
/// Complete source-neutral material required to construct one canonical RAW transaction.
pub struct RawTransactionMaterial {
block_time: std::option::Option<i64>,
meta: crate::RawTransactionWireField<serde_json::Value>,
network: ksp_store_api::RawNetworkId,
signature: ksp_store_api::RawTransactionSignature,
slot: u64,
transaction_data: std::string::String,
transaction_index: crate::RawTransactionWireField<u32>,
version: crate::RawTransactionWireField<crate::RawTransactionVersion>,
}
impl crate::RawTransactionMaterial {
/// Creates complete source-neutral material for the frozen RAW v1 binary Base64 body path.
#[must_use]
pub fn binary_base64(
network: ksp_store_api::RawNetworkId,
signature: ksp_store_api::RawTransactionSignature,
slot: u64,
block_time: std::option::Option<i64>,
transaction_data: impl std::convert::Into<std::string::String>,
meta: crate::RawTransactionWireField<serde_json::Value>,
version: crate::RawTransactionWireField<crate::RawTransactionVersion>,
transaction_index: crate::RawTransactionWireField<u32>,
) -> Self {
return Self {
block_time,
meta,
network,
signature,
slot,
transaction_data: transaction_data.into(),
transaction_index,
version,
};
}
/// Creates complete source-neutral RAW v1 material from a Base64 transaction wire that embeds its own canonical signature array.
pub fn binary_base64_with_embedded_signature(
network: ksp_store_api::RawNetworkId,
slot: u64,
block_time: std::option::Option<i64>,
transaction_data: impl std::convert::Into<std::string::String>,
meta: crate::RawTransactionWireField<serde_json::Value>,
version: crate::RawTransactionWireField<crate::RawTransactionVersion>,
transaction_index: crate::RawTransactionWireField<u32>,
) -> ksp_core_lib::Result<Self> {
let transaction_data = transaction_data.into();
let signature = crate::extract_raw_transaction_signature_from_binary_base64(transaction_data.as_str());
return match signature {
std::result::Result::Ok(signature) => {
std::result::Result::Ok(Self { block_time, meta, network, signature, slot, transaction_data, transaction_index, version })
},
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
}
impl std::fmt::Debug for crate::RawTransactionMaterial {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("RawTransactionMaterial")
.field("network", &self.network)
.field("slot", &self.slot)
.field("block_time", &self.block_time)
.field("transaction_data_len", &self.transaction_data.len())
.field("meta", &wire_field_state(&self.meta))
.field("version", &wire_field_state(&self.version))
.field("transaction_index", &wire_field_state(&self.transaction_index))
.finish_non_exhaustive();
}
}
/// Canonicalizes complete source-neutral transaction material into the frozen KSP RAW transaction v1 representation.
pub fn canonicalize_raw_transaction(material: crate::RawTransactionMaterial) -> ksp_core_lib::Result<ksp_store_api::RawTransaction> {
let block_time = convert_block_time(material.block_time);
let block_time = match block_time {
std::result::Result::Ok(block_time) => block_time,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let bytes = canonical_payload_bytes(&material);
let bytes = match bytes {
std::result::Result::Ok(bytes) => bytes,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if bytes.is_empty() || bytes.len() > ksp_store_api::MAX_RAW_PAYLOAD_BYTES {
return std::result::Result::Err(crate::canonicalization_error("payload.bytes"));
}
let hash: [u8; 32] = sha2::Sha256::digest(bytes.as_slice()).into();
let format_id = ksp_store_api::RawFormatId::new(crate::RAW_TRANSACTION_FORMAT_ID);
let format_id = match format_id {
std::result::Result::Ok(format_id) => format_id,
std::result::Result::Err(_) => return std::result::Result::Err(crate::canonicalization_error("payload.format_id")),
};
let payload = ksp_store_api::RawPayload::try_new(
format_id,
crate::RAW_TRANSACTION_FORMAT_VERSION,
bytes.into_boxed_slice(),
ksp_store_api::RawContentHash::new(hash),
);
let payload = match payload {
std::result::Result::Ok(payload) => payload,
std::result::Result::Err(_) => return std::result::Result::Err(crate::canonicalization_error("payload")),
};
let reference = ksp_store_api::RawTransactionReference::new(material.network, material.signature);
return std::result::Result::Ok(ksp_store_api::RawTransaction::new(reference, material.slot, block_time, payload));
}
fn append_canonical_json(output: &mut std::vec::Vec<u8>, value: &serde_json::Value) -> ksp_core_lib::Result<()> {
return match value {
serde_json::Value::Null => {
output.extend_from_slice(b"null");
std::result::Result::Ok(())
},
serde_json::Value::Bool(value) => {
if *value {
output.extend_from_slice(b"true");
} else {
output.extend_from_slice(b"false");
}
std::result::Result::Ok(())
},
serde_json::Value::Number(value) => {
output.extend_from_slice(value.to_string().as_bytes());
std::result::Result::Ok(())
},
serde_json::Value::String(value) => append_json_string(output, value.as_str()),
serde_json::Value::Array(values) => {
output.push(b'[');
for (index, item) in values.iter().enumerate() {
if index != 0 {
output.push(b',');
}
let item_result = append_canonical_json(output, item);
if let std::result::Result::Err(error) = item_result {
return std::result::Result::Err(error);
}
}
output.push(b']');
std::result::Result::Ok(())
},
serde_json::Value::Object(values) => {
output.push(b'{');
let mut keys = values.keys().collect::<std::vec::Vec<_>>();
keys.sort_unstable();
for (index, key) in keys.iter().enumerate() {
if index != 0 {
output.push(b',');
}
let key_result = append_json_string(output, key.as_str());
if let std::result::Result::Err(error) = key_result {
return std::result::Result::Err(error);
}
output.push(b':');
let item = values.get(key.as_str());
let item = match item {
std::option::Option::Some(item) => item,
std::option::Option::None => return std::result::Result::Err(crate::canonicalization_error("payload.meta")),
};
let item_result = append_canonical_json(output, item);
if let std::result::Result::Err(error) = item_result {
return std::result::Result::Err(error);
}
}
output.push(b'}');
std::result::Result::Ok(())
},
};
}
fn append_json_string(output: &mut std::vec::Vec<u8>, value: &str) -> ksp_core_lib::Result<()> {
let encoded = serde_json::to_vec(value);
return match encoded {
std::result::Result::Ok(encoded) => {
output.extend_from_slice(encoded.as_slice());
std::result::Result::Ok(())
},
std::result::Result::Err(_) => std::result::Result::Err(crate::canonicalization_error("payload.json")),
};
}
fn append_wire_value<T, F>(
output: &mut std::vec::Vec<u8>,
key: &str,
field: &crate::RawTransactionWireField<T>,
mut append_value: F,
) -> ksp_core_lib::Result<()>
where
F: FnMut(&mut std::vec::Vec<u8>, &T) -> ksp_core_lib::Result<()>,
{
return match field {
crate::RawTransactionWireField::Omitted => std::result::Result::Ok(()),
crate::RawTransactionWireField::Null => {
output.push(b',');
let key_result = append_json_string(output, key);
if let std::result::Result::Err(error) = key_result {
return std::result::Result::Err(error);
}
output.extend_from_slice(b":null");
std::result::Result::Ok(())
},
crate::RawTransactionWireField::Value(value) => {
output.push(b',');
let key_result = append_json_string(output, key);
if let std::result::Result::Err(error) = key_result {
return std::result::Result::Err(error);
}
output.push(b':');
append_value(output, value)
},
};
}
fn canonical_payload_bytes(material: &crate::RawTransactionMaterial) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
let mut bytes = std::vec::Vec::new();
bytes.extend_from_slice(b"{\"transaction\":[");
let transaction_result = append_json_string(&mut bytes, material.transaction_data.as_str());
if let std::result::Result::Err(error) = transaction_result {
return std::result::Result::Err(error);
}
bytes.push(b',');
let encoding_result = append_json_string(&mut bytes, "base64");
if let std::result::Result::Err(error) = encoding_result {
return std::result::Result::Err(error);
}
bytes.push(b']');
let meta_result = append_wire_value(&mut bytes, "meta", &material.meta, |output, value| return append_canonical_json(output, value));
if let std::result::Result::Err(error) = meta_result {
return std::result::Result::Err(error);
}
let version_result = append_wire_value(&mut bytes, "version", &material.version, |output, value| {
return match value {
crate::RawTransactionVersion::Legacy => append_json_string(output, "legacy"),
crate::RawTransactionVersion::Number(number) => {
output.extend_from_slice(number.to_string().as_bytes());
std::result::Result::Ok(())
},
};
});
if let std::result::Result::Err(error) = version_result {
return std::result::Result::Err(error);
}
let transaction_index_result = append_wire_value(&mut bytes, "transactionIndex", &material.transaction_index, |output, value| {
output.extend_from_slice(value.to_string().as_bytes());
return std::result::Result::Ok(());
});
if let std::result::Result::Err(error) = transaction_index_result {
return std::result::Result::Err(error);
}
bytes.push(b'}');
return std::result::Result::Ok(bytes);
}
fn convert_block_time(value: std::option::Option<i64>) -> ksp_core_lib::Result<std::option::Option<ksp_store_api::RawTimestamp>> {
let seconds = match value {
std::option::Option::Some(seconds) => seconds,
std::option::Option::None => return std::result::Result::Ok(std::option::Option::None),
};
let seconds = match u64::try_from(seconds) {
std::result::Result::Ok(seconds) => seconds,
std::result::Result::Err(_) => return std::result::Result::Err(crate::material_error("block_time")),
};
let millis = match seconds.checked_mul(1_000) {
std::option::Option::Some(millis) => millis,
std::option::Option::None => return std::result::Result::Err(crate::material_error("block_time")),
};
let timestamp = ksp_store_api::RawTimestamp::from_unix_millis(millis);
return match timestamp {
std::result::Result::Ok(timestamp) => std::result::Result::Ok(std::option::Option::Some(timestamp)),
std::result::Result::Err(_) => std::result::Result::Err(crate::material_error("block_time")),
};
}
fn wire_field_state<T>(field: &crate::RawTransactionWireField<T>) -> &'static str {
return match field {
crate::RawTransactionWireField::Omitted => "omitted",
crate::RawTransactionWireField::Null => "null",
crate::RawTransactionWireField::Value(_) => "value",
};
}
#[cfg(test)]
#[path = "../unit_tests/canonical.rs"]
mod tests;