217 lines
7.3 KiB
Rust
217 lines
7.3 KiB
Rust
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<String, BrowserCoreError> {
|
|
serde_json::to_string(self).map_err(|error| BrowserCoreError::Json(error.to_string()))
|
|
}
|
|
|
|
pub fn hash(&self) -> Result<Vec<u8>, 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<Vec<u8>, 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<Self, BrowserCoreError> {
|
|
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<BrowserTransferOutput, BrowserCoreError> {
|
|
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);
|
|
}
|
|
}
|