use serde::{Deserialize, Serialize}; use crate::address; use crate::crypto; use crate::error::BrowserCoreError; use crate::SIGNATURE_LENGTH; pub const TRANSFER_TYPE: u8 = 2; pub const COIN_BYTES: usize = 15; pub const TRANSFER_BYTES: usize = 750; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct UnsignedTransfer { pub txtype: u8, pub time: u32, pub value: u64, pub coin: String, pub nft_series: u32, pub sender: String, pub receiver: String, pub txfee: u64, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct SignedTransfer { pub unsigned_transfer: UnsignedTransfer, pub signature: String, } #[derive(Debug, Serialize)] pub struct BrowserTransferOutput { pub txtype: u8, pub time: u32, pub value: String, pub coin: String, pub nft_series: u32, pub sender: String, pub receiver: String, pub txfee: String, pub hash: String, pub signature: String, pub bytes_hex: String, } impl UnsignedTransfer { pub fn validate(&self) -> Result<(), BrowserCoreError> { if self.txtype != TRANSFER_TYPE { return Err(BrowserCoreError::InvalidTransfer( "transaction type must be 2".into(), )); } if self.coin.len() != COIN_BYTES || !self.coin.is_ascii() { return Err(BrowserCoreError::InvalidTransfer( "coin must use the canonical 15-byte padded format".into(), )); } address::short_address_to_bytes(&self.sender)?; address::short_address_to_bytes(&self.receiver)?; Ok(()) } pub fn json(&self) -> Result { serde_json::to_string(self).map_err(|error| BrowserCoreError::Json(error.to_string())) } pub fn hash(&self) -> Result, BrowserCoreError> { Ok(crypto::skein_256(self.json()?.as_bytes())) } } impl SignedTransfer { pub fn validate_shape(&self) -> Result<(), BrowserCoreError> { self.unsigned_transfer.validate()?; let signature = hex::decode(&self.signature).map_err(|_| BrowserCoreError::InvalidSignature)?; if signature.len() != SIGNATURE_LENGTH { return Err(BrowserCoreError::InvalidSignature); } Ok(()) } pub fn to_bytes(&self) -> Result, BrowserCoreError> { self.validate_shape()?; let mut bytes = Vec::with_capacity(TRANSFER_BYTES); bytes.push(self.unsigned_transfer.txtype); bytes.extend_from_slice(&self.unsigned_transfer.time.to_le_bytes()); bytes.extend_from_slice(&self.unsigned_transfer.value.to_le_bytes()); bytes.extend_from_slice(self.unsigned_transfer.coin.as_bytes()); bytes.extend_from_slice(&self.unsigned_transfer.nft_series.to_le_bytes()); bytes.extend_from_slice(&address::short_address_to_bytes( &self.unsigned_transfer.sender, )?); bytes.extend_from_slice(&address::short_address_to_bytes( &self.unsigned_transfer.receiver, )?); bytes.extend_from_slice(&self.unsigned_transfer.txfee.to_le_bytes()); bytes.extend_from_slice( &hex::decode(&self.signature).map_err(|_| BrowserCoreError::InvalidSignature)?, ); debug_assert_eq!(bytes.len(), TRANSFER_BYTES); Ok(bytes) } pub fn from_bytes(bytes: &[u8]) -> Result { if bytes.len() != TRANSFER_BYTES || bytes[0] != TRANSFER_TYPE { return Err(BrowserCoreError::InvalidTransfer(format!( "expected a {TRANSFER_BYTES}-byte type-2 transaction" ))); } let mut offset = 1; let time = read_u32(bytes, &mut offset); let value = read_u64(bytes, &mut offset); let coin = String::from_utf8(bytes[offset..offset + COIN_BYTES].to_vec()) .map_err(|_| BrowserCoreError::InvalidTransfer("coin is not UTF-8".into()))?; offset += COIN_BYTES; let nft_series = read_u32(bytes, &mut offset); let sender = address::bytes_to_short_address(&bytes[offset..offset + 22])?; offset += 22; let receiver = address::bytes_to_short_address(&bytes[offset..offset + 22])?; offset += 22; let txfee = read_u64(bytes, &mut offset); let signature = hex::encode(&bytes[offset..offset + SIGNATURE_LENGTH]); let transfer = Self { unsigned_transfer: UnsignedTransfer { txtype: TRANSFER_TYPE, time, value, coin, nft_series, sender, receiver, txfee, }, signature, }; transfer.validate_shape()?; Ok(transfer) } pub fn browser_output(&self) -> Result { let bytes_hex = hex::encode(self.to_bytes()?); let hash = hex::encode(self.unsigned_transfer.hash()?); Ok(BrowserTransferOutput { txtype: self.unsigned_transfer.txtype, time: self.unsigned_transfer.time, value: self.unsigned_transfer.value.to_string(), coin: self.unsigned_transfer.coin.clone(), nft_series: self.unsigned_transfer.nft_series, sender: self.unsigned_transfer.sender.clone(), receiver: self.unsigned_transfer.receiver.clone(), txfee: self.unsigned_transfer.txfee.to_string(), hash, signature: self.signature.clone(), bytes_hex, }) } } fn read_u32(bytes: &[u8], offset: &mut usize) -> u32 { let value = u32::from_le_bytes(bytes[*offset..*offset + 4].try_into().unwrap()); *offset += 4; value } fn read_u64(bytes: &[u8], offset: &mut usize) -> u64 { let value = u64::from_le_bytes(bytes[*offset..*offset + 8].try_into().unwrap()); *offset += 8; value } #[cfg(test)] mod tests { use super::*; fn address(byte: u8) -> String { format!("{}.{}", hex::encode([byte; 20]), crate::address::network_suffix()) } fn unsigned() -> UnsignedTransfer { UnsignedTransfer { txtype: TRANSFER_TYPE, time: 1_700_000_000, value: 123_456_789, coin: "CLTC ".to_string(), nft_series: 0, sender: address(0x11), receiver: address(0x22), txfee: 1_234_568, } } #[test] fn unsigned_json_matches_node_field_order() { let serialized = unsigned().json().unwrap(); assert!(serialized.contains(&format!("1111111111111111111111111111111111111111.{}", crate::address::network_suffix()))); assert!(serialized.contains(&format!("2222222222222222222222222222222222222222.{}", crate::address::network_suffix()))); #[cfg(feature = "testnet")] assert_eq!( hex::encode(unsigned().hash().unwrap()), "7e6a1ef0232eee327dd6c18640f85b16e632762a7c424abfc720ee72fb002c32" ); } #[test] fn transfer_wire_layout_round_trips() { let transfer = SignedTransfer { unsigned_transfer: unsigned(), signature: hex::encode([0x33; SIGNATURE_LENGTH]), }; let bytes = transfer.to_bytes().unwrap(); assert_eq!(bytes.len(), TRANSFER_BYTES); assert_eq!(SignedTransfer::from_bytes(&bytes).unwrap(), transfer); } }