v0.3.10-pre.002
This commit is contained in:
49
crates/ksp-raw-transaction-lib/src/acquisition.rs
Normal file
49
crates/ksp-raw-transaction-lib/src/acquisition.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
// file: crates/ksp-raw-transaction-lib/src/acquisition.rs
|
||||
// version: 1
|
||||
|
||||
/// Complete source-neutral RAW transaction acquisition containing one canonical entity and one producer-owned observation.
|
||||
pub struct RawTransactionAcquisition {
|
||||
observation: ksp_store_api::RawTransactionObservation,
|
||||
transaction: ksp_store_api::RawTransaction,
|
||||
}
|
||||
|
||||
impl crate::RawTransactionAcquisition {
|
||||
/// Returns the producer-owned acquisition observation linked to the canonical transaction.
|
||||
#[must_use]
|
||||
pub const fn observation(&self) -> &ksp_store_api::RawTransactionObservation {
|
||||
return &self.observation;
|
||||
}
|
||||
|
||||
/// Returns the canonical source-independent RAW transaction.
|
||||
#[must_use]
|
||||
pub const fn transaction(&self) -> &ksp_store_api::RawTransaction {
|
||||
return &self.transaction;
|
||||
}
|
||||
|
||||
/// Consumes this acquisition into its canonical transaction and observation.
|
||||
#[must_use]
|
||||
pub fn into_parts(self) -> (ksp_store_api::RawTransaction, ksp_store_api::RawTransactionObservation) {
|
||||
return (self.transaction, self.observation);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for crate::RawTransactionAcquisition {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter.write_str("RawTransactionAcquisition(..)");
|
||||
}
|
||||
}
|
||||
|
||||
/// Assembles one canonical transaction with a producer-owned observation key and safe acquisition provenance.
|
||||
#[must_use]
|
||||
pub fn assemble_raw_transaction_acquisition(
|
||||
transaction: ksp_store_api::RawTransaction,
|
||||
observation_key: ksp_store_api::RawObservationKey,
|
||||
provenance: ksp_store_api::RawAcquisitionProvenance,
|
||||
) -> crate::RawTransactionAcquisition {
|
||||
let observation = ksp_store_api::RawTransactionObservation::new(observation_key, transaction.reference().clone(), provenance);
|
||||
return crate::RawTransactionAcquisition { observation, transaction };
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/acquisition.rs"]
|
||||
mod tests;
|
||||
305
crates/ksp-raw-transaction-lib/src/canonical.rs
Normal file
305
crates/ksp-raw-transaction-lib/src/canonical.rs
Normal file
@@ -0,0 +1,305 @@
|
||||
// file: crates/ksp-raw-transaction-lib/src/canonical.rs
|
||||
// version: 1
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
27
crates/ksp-raw-transaction-lib/src/error.rs
Normal file
27
crates/ksp-raw-transaction-lib/src/error.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
// file: crates/ksp-raw-transaction-lib/src/error.rs
|
||||
// version: 1
|
||||
|
||||
/// Error code used when RAW v1 canonical payload construction cannot preserve the frozen format contract.
|
||||
pub const ERROR_CODE_RAW_TRANSACTION_CANONICALIZATION_INVALID: ksp_core_lib::ErrorCode =
|
||||
ksp_core_lib::ErrorCode::new("raw_transaction", "canonicalization_invalid");
|
||||
/// Error code used when source-neutral RAW transaction material violates a bounded semantic invariant.
|
||||
pub const ERROR_CODE_RAW_TRANSACTION_MATERIAL_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("raw_transaction", "material_invalid");
|
||||
/// Error code used when textual Solana transaction signature material cannot decode to exactly 64 canonical bytes.
|
||||
pub const ERROR_CODE_RAW_TRANSACTION_SIGNATURE_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("raw_transaction", "signature_invalid");
|
||||
|
||||
/// Creates a safe canonicalization error without copying source payload material.
|
||||
pub(crate) fn canonicalization_error(field: &'static str) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_RAW_TRANSACTION_CANONICALIZATION_INVALID, "invalid canonical RAW transaction representation")
|
||||
.with_context("field", field);
|
||||
}
|
||||
|
||||
/// Creates a safe source-material error without copying source payload material.
|
||||
pub(crate) fn material_error(field: &'static str) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_RAW_TRANSACTION_MATERIAL_INVALID, "invalid source-neutral RAW transaction material")
|
||||
.with_context("field", field);
|
||||
}
|
||||
|
||||
/// Creates a safe signature error without copying textual signature material.
|
||||
pub(crate) fn signature_error() -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_RAW_TRANSACTION_SIGNATURE_INVALID, "invalid canonical Solana transaction signature");
|
||||
}
|
||||
54
crates/ksp-raw-transaction-lib/src/lib.rs
Normal file
54
crates/ksp-raw-transaction-lib/src/lib.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
// file: crates/ksp-raw-transaction-lib/src/lib.rs
|
||||
// version: 1
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Source-neutral canonicalization of complete Solana transactions into KSP RAW transaction v1.
|
||||
//!
|
||||
//! This lower layer owns the frozen RAW transaction format, textual signature parsing, canonical
|
||||
//! JSON construction, content hashing and assembly of one canonical transaction with one
|
||||
//! producer-owned observation. It owns no Transport, Config, Job, Worker, async runtime or Store
|
||||
//! backend behavior.
|
||||
|
||||
mod acquisition;
|
||||
mod canonical;
|
||||
mod error;
|
||||
mod signature;
|
||||
|
||||
/// Complete source-neutral RAW transaction acquisition containing one canonical entity and one producer-owned observation.
|
||||
pub use self::acquisition::RawTransactionAcquisition;
|
||||
/// Assembles one canonical transaction with a producer-owned observation key and safe acquisition provenance.
|
||||
pub use self::acquisition::assemble_raw_transaction_acquisition;
|
||||
/// KSP-owned source-independent RAW transaction format identifier.
|
||||
pub use self::canonical::RAW_TRANSACTION_FORMAT_ID;
|
||||
/// Frozen KSP-owned RAW transaction format version.
|
||||
pub use self::canonical::RAW_TRANSACTION_FORMAT_VERSION;
|
||||
/// Complete source-neutral material required to construct one canonical RAW transaction.
|
||||
pub use self::canonical::RawTransactionMaterial;
|
||||
/// Canonical Solana transaction version admitted by RAW transaction format v1.
|
||||
pub use self::canonical::RawTransactionVersion;
|
||||
/// Source-wire presence state preserving omitted, explicit null and concrete values independently.
|
||||
pub use self::canonical::RawTransactionWireField;
|
||||
/// Canonicalizes complete source-neutral transaction material into the frozen KSP RAW transaction v1 representation.
|
||||
pub use self::canonical::canonicalize_raw_transaction;
|
||||
/// Error code used when RAW v1 canonical payload construction cannot preserve the frozen format contract.
|
||||
pub use self::error::ERROR_CODE_RAW_TRANSACTION_CANONICALIZATION_INVALID;
|
||||
/// Error code used when source-neutral RAW transaction material violates a bounded semantic invariant.
|
||||
pub use self::error::ERROR_CODE_RAW_TRANSACTION_MATERIAL_INVALID;
|
||||
/// Error code used when textual Solana transaction signature material cannot decode to exactly 64 canonical bytes.
|
||||
pub use self::error::ERROR_CODE_RAW_TRANSACTION_SIGNATURE_INVALID;
|
||||
/// Maximum UTF-8 byte length admitted for one textual Base58 Solana transaction signature.
|
||||
pub use self::signature::MAX_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES;
|
||||
/// Minimum UTF-8 byte length admitted for one textual Base58 Solana transaction signature.
|
||||
pub use self::signature::MIN_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES;
|
||||
/// Parses one bounded Base58 Solana transaction signature to exactly 64 canonical bytes.
|
||||
pub use self::signature::parse_raw_transaction_signature;
|
||||
|
||||
/// Creates a safe canonicalization error without copying source payload material.
|
||||
pub(crate) use self::error::canonicalization_error;
|
||||
/// Creates a safe source-material error without copying source payload material.
|
||||
pub(crate) use self::error::material_error;
|
||||
/// Creates a safe signature error without copying textual signature material.
|
||||
pub(crate) use self::error::signature_error;
|
||||
67
crates/ksp-raw-transaction-lib/src/signature.rs
Normal file
67
crates/ksp-raw-transaction-lib/src/signature.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
// file: crates/ksp-raw-transaction-lib/src/signature.rs
|
||||
// version: 1
|
||||
|
||||
/// Maximum UTF-8 byte length admitted for one textual Base58 Solana transaction signature.
|
||||
pub const MAX_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES: usize = 88;
|
||||
/// Minimum UTF-8 byte length admitted for one textual Base58 Solana transaction signature.
|
||||
pub const MIN_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES: usize = 64;
|
||||
|
||||
/// Parses one bounded Base58 Solana transaction signature to exactly 64 canonical bytes.
|
||||
pub fn parse_raw_transaction_signature(value: &str) -> ksp_core_lib::Result<ksp_store_api::RawTransactionSignature> {
|
||||
let text = value.as_bytes();
|
||||
if text.len() < crate::MIN_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES || text.len() > crate::MAX_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES {
|
||||
return std::result::Result::Err(
|
||||
crate::signature_error()
|
||||
.with_context("actual_len", text.len().to_string())
|
||||
.with_context("minimum_len", crate::MIN_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES.to_string())
|
||||
.with_context("maximum_len", crate::MAX_RAW_TRANSACTION_SIGNATURE_TEXT_BYTES.to_string()),
|
||||
);
|
||||
}
|
||||
let mut decoded = [0_u8; 64];
|
||||
let mut leading_zeroes = 0_usize;
|
||||
for byte in text {
|
||||
if *byte != b'1' {
|
||||
break;
|
||||
}
|
||||
leading_zeroes += 1;
|
||||
}
|
||||
for byte in text {
|
||||
let digit = match base58_digit(*byte) {
|
||||
std::option::Option::Some(digit) => digit,
|
||||
std::option::Option::None => return std::result::Result::Err(crate::signature_error()),
|
||||
};
|
||||
let mut carry = u32::from(digit);
|
||||
for output in decoded.iter_mut().rev() {
|
||||
let expanded = (u32::from(*output) * 58) + carry;
|
||||
*output = (expanded & 0xff) as u8;
|
||||
carry = expanded >> 8;
|
||||
}
|
||||
if carry != 0 {
|
||||
return std::result::Result::Err(crate::signature_error());
|
||||
}
|
||||
}
|
||||
let significant_len = match decoded.iter().position(|byte| return *byte != 0) {
|
||||
std::option::Option::Some(index) => decoded.len() - index,
|
||||
std::option::Option::None => 0,
|
||||
};
|
||||
if leading_zeroes + significant_len != decoded.len() {
|
||||
return std::result::Result::Err(crate::signature_error());
|
||||
}
|
||||
return std::result::Result::Ok(ksp_store_api::RawTransactionSignature::new(decoded));
|
||||
}
|
||||
|
||||
fn base58_digit(byte: u8) -> std::option::Option<u8> {
|
||||
return match byte {
|
||||
b'1'..=b'9' => std::option::Option::Some(byte - b'1'),
|
||||
b'A'..=b'H' => std::option::Option::Some((byte - b'A') + 9),
|
||||
b'J'..=b'N' => std::option::Option::Some((byte - b'J') + 17),
|
||||
b'P'..=b'Z' => std::option::Option::Some((byte - b'P') + 22),
|
||||
b'a'..=b'k' => std::option::Option::Some((byte - b'a') + 33),
|
||||
b'm'..=b'z' => std::option::Option::Some((byte - b'm') + 44),
|
||||
_ => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/signature.rs"]
|
||||
mod tests;
|
||||
Reference in New Issue
Block a user