Contractless-Web3-Wallets/core/src/transfer.rs

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);
}
}