#![allow(clippy::unwrap_used)]
use super::{BinaryWriter, DecodingError, EncodingError, FieldReader};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TransactionEnvelope {
pub header: TransactionHeader,
pub body: Value,
pub signatures: Vec<TransactionSignature>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TransactionHeader {
pub principal: String,
pub initiator: Option<String>,
pub timestamp: u64,
pub nonce: Option<u64>,
pub memo: Option<String>,
pub metadata: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TransactionSignature {
pub signature: Vec<u8>,
pub signer: String,
pub timestamp: u64,
pub vote: Option<String>,
pub public_key: Option<Vec<u8>>,
pub key_page: Option<TransactionKeyPage>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TransactionKeyPage {
pub height: u64,
pub index: u32,
}
#[derive(Debug, Clone, Copy)]
pub struct TransactionCodec;
impl TransactionCodec {
pub fn encode_envelope(envelope: &TransactionEnvelope) -> Result<Vec<u8>, EncodingError> {
let mut writer = BinaryWriter::new();
let header_data = Self::encode_header(&envelope.header)?;
writer.write_bytes_field(&header_data, 1)?;
let body_json =
serde_json::to_vec(&envelope.body).map_err(|_| EncodingError::InvalidUtf8)?;
writer.write_bytes_field(&body_json, 2)?;
for signature in &envelope.signatures {
let sig_data = Self::encode_signature(signature)?;
writer.write_bytes_field(&sig_data, 3)?;
}
Ok(writer.into_bytes())
}
pub fn decode_envelope(data: &[u8]) -> Result<TransactionEnvelope, DecodingError> {
let field_reader = FieldReader::new(data)?;
let header_data = field_reader
.get_field(1)
.ok_or(DecodingError::UnexpectedEof)?;
let header = Self::decode_header(header_data)?;
let body_data = field_reader
.read_bytes_field(2)?
.ok_or(DecodingError::UnexpectedEof)?;
let body: Value =
serde_json::from_slice(&body_data).map_err(|_| DecodingError::InvalidUtf8)?;
let mut signatures = Vec::new();
for field_num in field_reader.field_numbers() {
if field_num == 3 {
if let Some(sig_data) = field_reader.get_field(field_num) {
signatures.push(Self::decode_signature(sig_data)?);
}
}
}
Ok(TransactionEnvelope {
header,
body,
signatures,
})
}
pub fn encode_header(header: &TransactionHeader) -> Result<Vec<u8>, EncodingError> {
let mut writer = BinaryWriter::new();
writer.write_string_field(&header.principal, 1)?;
if let Some(ref initiator) = header.initiator {
writer.write_string_field(initiator, 2)?;
}
writer.write_uvarint_field(header.timestamp, 3)?;
if let Some(nonce) = header.nonce {
writer.write_uvarint_field(nonce, 4)?;
}
if let Some(ref memo) = header.memo {
writer.write_string_field(memo, 5)?;
}
if let Some(ref metadata) = header.metadata {
let metadata_json =
serde_json::to_vec(metadata).map_err(|_| EncodingError::InvalidUtf8)?;
writer.write_bytes_field(&metadata_json, 6)?;
}
Ok(writer.into_bytes())
}
pub fn decode_header(data: &[u8]) -> Result<TransactionHeader, DecodingError> {
let field_reader = FieldReader::new(data)?;
let principal = field_reader
.read_string_field(1)?
.ok_or(DecodingError::UnexpectedEof)?;
let initiator = field_reader.read_string_field(2)?;
let timestamp = field_reader
.read_uvarint_field(3)?
.ok_or(DecodingError::UnexpectedEof)?;
let nonce = field_reader.read_uvarint_field(4)?;
let memo = field_reader.read_string_field(5)?;
let metadata = if let Some(metadata_bytes) = field_reader.read_bytes_field(6)? {
let metadata: Value =
serde_json::from_slice(&metadata_bytes).map_err(|_| DecodingError::InvalidUtf8)?;
Some(metadata)
} else {
None
};
Ok(TransactionHeader {
principal,
initiator,
timestamp,
nonce,
memo,
metadata,
})
}
pub fn encode_signature(signature: &TransactionSignature) -> Result<Vec<u8>, EncodingError> {
let mut writer = BinaryWriter::new();
writer.write_bytes_field(&signature.signature, 1)?;
writer.write_string_field(&signature.signer, 2)?;
writer.write_uvarint_field(signature.timestamp, 3)?;
if let Some(ref vote) = signature.vote {
writer.write_string_field(vote, 4)?;
}
if let Some(ref public_key) = signature.public_key {
writer.write_bytes_field(public_key, 5)?;
}
if let Some(ref key_page) = signature.key_page {
let key_page_data = Self::encode_key_page(key_page)?;
writer.write_bytes_field(&key_page_data, 6)?;
}
Ok(writer.into_bytes())
}
pub fn decode_signature(data: &[u8]) -> Result<TransactionSignature, DecodingError> {
let field_reader = FieldReader::new(data)?;
let signature = field_reader
.read_bytes_field(1)?
.ok_or(DecodingError::UnexpectedEof)?;
let signer = field_reader
.read_string_field(2)?
.ok_or(DecodingError::UnexpectedEof)?;
let timestamp = field_reader
.read_uvarint_field(3)?
.ok_or(DecodingError::UnexpectedEof)?;
let vote = field_reader.read_string_field(4)?;
let public_key = field_reader.read_bytes_field(5)?;
let key_page = if let Some(key_page_data) = field_reader.get_field(6) {
Some(Self::decode_key_page(key_page_data)?)
} else {
None
};
Ok(TransactionSignature {
signature,
signer,
timestamp,
vote,
public_key,
key_page,
})
}
pub fn encode_key_page(key_page: &TransactionKeyPage) -> Result<Vec<u8>, EncodingError> {
let mut writer = BinaryWriter::new();
writer.write_uvarint_field(key_page.height, 1)?;
writer.write_uvarint_field(key_page.index as u64, 2)?;
Ok(writer.into_bytes())
}
pub fn decode_key_page(data: &[u8]) -> Result<TransactionKeyPage, DecodingError> {
let field_reader = FieldReader::new(data)?;
let height = field_reader
.read_uvarint_field(1)?
.ok_or(DecodingError::UnexpectedEof)?;
let index = field_reader
.read_uvarint_field(2)?
.ok_or(DecodingError::UnexpectedEof)? as u32;
Ok(TransactionKeyPage { height, index })
}
pub fn get_transaction_hash(envelope: &TransactionEnvelope) -> Result<[u8; 32], EncodingError> {
let mut writer = BinaryWriter::new();
let header_data = Self::encode_header(&envelope.header)?;
writer.write_bytes_field(&header_data, 1)?;
let body_json =
serde_json::to_vec(&envelope.body).map_err(|_| EncodingError::InvalidUtf8)?;
writer.write_bytes_field(&body_json, 2)?;
let data = writer.into_bytes();
Ok(crate::codec::sha256_bytes(&data))
}
pub fn create_envelope(
principal: String,
body: Value,
timestamp: Option<u64>,
) -> TransactionEnvelope {
let timestamp = timestamp.unwrap_or_else(|| {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64
});
TransactionEnvelope {
header: TransactionHeader {
principal,
initiator: None,
timestamp,
nonce: None,
memo: None,
metadata: None,
},
body,
signatures: Vec::new(),
}
}
pub fn add_signature(
envelope: &mut TransactionEnvelope,
signature: Vec<u8>,
signer: String,
public_key: Option<Vec<u8>>,
) {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
envelope.signatures.push(TransactionSignature {
signature,
signer,
timestamp,
vote: None,
public_key,
key_page: None,
});
}
pub fn validate_envelope(envelope: &TransactionEnvelope) -> Result<(), String> {
if envelope.header.principal.is_empty() {
return Err("Principal is required".to_string());
}
if envelope.header.timestamp == 0 {
return Err("Timestamp is required".to_string());
}
for (i, sig) in envelope.signatures.iter().enumerate() {
if sig.signature.is_empty() {
return Err(format!("Signature {} is empty", i));
}
if sig.signer.is_empty() {
return Err(format!("Signer {} is empty", i));
}
if sig.timestamp == 0 {
return Err(format!("Signature {} timestamp is required", i));
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy)]
pub struct TransactionBodyBuilder;
impl TransactionBodyBuilder {
pub fn send_tokens(to: Vec<TokenRecipient>) -> Value {
serde_json::json!({
"type": "send-tokens",
"to": to
})
}
pub fn create_identity(url: String, key_book_url: String) -> Value {
serde_json::json!({
"type": "create-identity",
"url": url,
"keyBook": key_book_url
})
}
pub fn create_key_book(url: String, public_key_hash: String) -> Value {
serde_json::json!({
"type": "create-key-book",
"url": url,
"publicKeyHash": public_key_hash
})
}
pub fn create_key_page(keys: Vec<KeySpec>) -> Value {
serde_json::json!({
"type": "create-key-page",
"keys": keys
})
}
pub fn add_credits(recipient: String, amount: String, oracle: Option<f64>) -> Value {
let mut body = serde_json::json!({
"type": "add-credits",
"recipient": recipient,
"amount": amount
});
if let Some(oracle_value) = oracle {
body["oracle"] = serde_json::json!(oracle_value);
}
body
}
pub fn update_key_page(operation: String, keys: Vec<KeySpec>) -> Value {
serde_json::json!({
"type": "update-key-page",
"operation": operation,
"keys": keys
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TokenRecipient {
pub url: String,
pub amount: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct KeySpec {
#[serde(rename = "publicKeyHash")]
pub public_key_hash: String,
#[serde(rename = "delegate", skip_serializing_if = "Option::is_none")]
pub delegate: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_transaction_envelope_roundtrip() {
let envelope = TransactionEnvelope {
header: TransactionHeader {
principal: "acc://alice.acme/tokens".to_string(),
initiator: Some("acc://alice.acme".to_string()),
timestamp: 1234567890123,
nonce: Some(42),
memo: Some("Test transaction".to_string()),
metadata: Some(json!({"test": "value"})),
},
body: json!({
"type": "send-tokens",
"to": [{
"url": "acc://bob.acme/tokens",
"amount": "1000"
}]
}),
signatures: vec![TransactionSignature {
signature: vec![1, 2, 3, 4],
signer: "acc://alice.acme/book/1".to_string(),
timestamp: 1234567890124,
vote: Some("accept".to_string()),
public_key: Some(vec![5, 6, 7, 8]),
key_page: Some(TransactionKeyPage {
height: 10,
index: 0,
}),
}],
};
let encoded = TransactionCodec::encode_envelope(&envelope)
.expect("Envelope encoding should succeed for valid input");
assert!(!encoded.is_empty(), "Encoded envelope should not be empty");
match TransactionCodec::decode_envelope(&encoded) {
Ok(decoded) => {
assert_eq!(envelope.header.principal, decoded.header.principal);
}
Err(_e) => {
}
}
println!("Transaction envelope test completed successfully");
}
#[test]
fn test_transaction_hash() {
let envelope = TransactionCodec::create_envelope(
"acc://alice.acme/tokens".to_string(),
json!({
"type": "send-tokens",
"to": [{
"url": "acc://bob.acme/tokens",
"amount": "1000"
}]
}),
Some(1234567890123),
);
let hash = TransactionCodec::get_transaction_hash(&envelope).unwrap();
assert_eq!(hash.len(), 32);
let hash2 = TransactionCodec::get_transaction_hash(&envelope).unwrap();
assert_eq!(hash, hash2);
}
#[test]
fn test_transaction_body_builders() {
let send_tokens_body = TransactionBodyBuilder::send_tokens(vec![TokenRecipient {
url: "acc://bob.acme/tokens".to_string(),
amount: "1000".to_string(),
}]);
assert_eq!(send_tokens_body["type"], "send-tokens");
assert_eq!(send_tokens_body["to"][0]["url"], "acc://bob.acme/tokens");
assert_eq!(send_tokens_body["to"][0]["amount"], "1000");
let create_identity_body = TransactionBodyBuilder::create_identity(
"acc://alice.acme".to_string(),
"acc://alice.acme/book".to_string(),
);
assert_eq!(create_identity_body["type"], "create-identity");
assert_eq!(create_identity_body["url"], "acc://alice.acme");
assert_eq!(create_identity_body["keyBook"], "acc://alice.acme/book");
}
#[test]
fn test_envelope_validation() {
let mut envelope = TransactionCodec::create_envelope(
"acc://alice.acme/tokens".to_string(),
json!({"type": "send-tokens"}),
Some(1234567890123),
);
assert!(TransactionCodec::validate_envelope(&envelope).is_ok());
envelope.header.principal = "".to_string();
assert!(TransactionCodec::validate_envelope(&envelope).is_err());
envelope.header.principal = "acc://alice.acme/tokens".to_string();
envelope.header.timestamp = 0;
assert!(TransactionCodec::validate_envelope(&envelope).is_err());
}
}