use serde::{Deserialize, Serialize};
use serde_big_array::BigArray;
use crate::{Address, Balance, Hash};
pub const SRC201_MAGIC: [u8; 4] = [0x53, 0x32, 0x30, 0x31];
pub const SRC201_VERSION: u8 = 1;
pub const SRC201_HEADER_SIZE: usize = 72;
pub const SRC201_NONCE_SIZE: usize = 24;
pub const SRC201_TAG_SIZE: usize = 16;
pub const SRC201_KDF_CONTEXT: &str = "SRC-201-v1.1-message-key";
pub const SRC201_ATTACHMENT_KDF_CONTEXT: &str = "SRC-201-v1.1-attachment-key";
pub const DEFAULT_DAILY_QUOTA: u32 = 100;
pub const DEFAULT_MAX_MESSAGE_SIZE: u32 = 65535;
pub const DEFAULT_MIN_TRUST_STAKE: u128 = 100_000_000_000;
pub const DEFAULT_SPAM_THRESHOLD: u32 = 50;
pub const DEFAULT_HIGH_SPAM_THRESHOLD: u32 = 80;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum MessagingOperation {
SendMessage = 0,
SendMessageDirect = 1,
SendMessageWithPayment = 2,
ClaimPayment = 3,
StakeForTrust = 4,
Unstake = 5,
SetInboxFilter = 6,
AddContact = 7,
RemoveContact = 8,
BlockSender = 9,
ReportSpam = 10,
RegisterPublicKey = 11,
UpdatePublicKey = 12,
SetDailyQuota = 128,
SetMaxMessageSize = 129,
SetMinTrustStake = 130,
SetSponsorshipEnabled = 131,
FundRegistry = 132,
}
impl MessagingOperation {
pub fn from_byte(b: u8) -> Option<Self> {
match b {
0 => Some(MessagingOperation::SendMessage),
1 => Some(MessagingOperation::SendMessageDirect),
2 => Some(MessagingOperation::SendMessageWithPayment),
3 => Some(MessagingOperation::ClaimPayment),
4 => Some(MessagingOperation::StakeForTrust),
5 => Some(MessagingOperation::Unstake),
6 => Some(MessagingOperation::SetInboxFilter),
7 => Some(MessagingOperation::AddContact),
8 => Some(MessagingOperation::RemoveContact),
9 => Some(MessagingOperation::BlockSender),
10 => Some(MessagingOperation::ReportSpam),
11 => Some(MessagingOperation::RegisterPublicKey),
12 => Some(MessagingOperation::UpdatePublicKey),
128 => Some(MessagingOperation::SetDailyQuota),
129 => Some(MessagingOperation::SetMaxMessageSize),
130 => Some(MessagingOperation::SetMinTrustStake),
131 => Some(MessagingOperation::SetSponsorshipEnabled),
132 => Some(MessagingOperation::FundRegistry),
_ => None,
}
}
pub fn is_admin(&self) -> bool {
(*self as u8) >= 128
}
pub fn is_send(&self) -> bool {
matches!(
self,
MessagingOperation::SendMessage
| MessagingOperation::SendMessageDirect
| MessagingOperation::SendMessageWithPayment
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MessagingTxData {
pub operation: MessagingOperation,
pub data: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct MessageFlags(pub u8);
impl MessageFlags {
pub const ENCRYPTED: u8 = 1 << 0;
pub const HAS_REPLY_TO: u8 = 1 << 1;
pub const HAS_TIMESTAMP: u8 = 1 << 2;
pub const HAS_ATTACHMENTS: u8 = 1 << 3;
pub const IS_READ_RECEIPT: u8 = 1 << 4;
pub const IS_PAYMENT_REQUEST: u8 = 1 << 5;
pub const REQUIRES_STAKE: u8 = 1 << 6;
pub fn new() -> Self {
Self(0)
}
pub fn encrypted() -> Self {
Self(Self::ENCRYPTED)
}
pub fn is_encrypted(&self) -> bool {
self.0 & Self::ENCRYPTED != 0
}
pub fn has_reply_to(&self) -> bool {
self.0 & Self::HAS_REPLY_TO != 0
}
pub fn has_timestamp(&self) -> bool {
self.0 & Self::HAS_TIMESTAMP != 0
}
pub fn has_attachments(&self) -> bool {
self.0 & Self::HAS_ATTACHMENTS != 0
}
pub fn is_read_receipt(&self) -> bool {
self.0 & Self::IS_READ_RECEIPT != 0
}
pub fn is_payment_request(&self) -> bool {
self.0 & Self::IS_PAYMENT_REQUEST != 0
}
pub fn requires_stake(&self) -> bool {
self.0 & Self::REQUIRES_STAKE != 0
}
pub fn set(&mut self, flag: u8) {
self.0 |= flag;
}
pub fn clear(&mut self, flag: u8) {
self.0 &= !flag;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum ContentType {
TextPlain = 0x01,
TextMarkdown = 0x02,
TextHtml = 0x03,
ApplicationJson = 0x10,
ApplicationPdf = 0x11,
ImagePng = 0x30,
ImageJpeg = 0x31,
ImageGif = 0x32,
ImageWebp = 0x33,
PaymentRequest = 0x80,
ReadReceipt = 0x81,
ContactCard = 0x82,
Custom = 0xFF,
}
impl ContentType {
pub fn from_byte(b: u8) -> Option<Self> {
match b {
0x01 => Some(ContentType::TextPlain),
0x02 => Some(ContentType::TextMarkdown),
0x03 => Some(ContentType::TextHtml),
0x10 => Some(ContentType::ApplicationJson),
0x11 => Some(ContentType::ApplicationPdf),
0x30 => Some(ContentType::ImagePng),
0x31 => Some(ContentType::ImageJpeg),
0x32 => Some(ContentType::ImageGif),
0x33 => Some(ContentType::ImageWebp),
0x80 => Some(ContentType::PaymentRequest),
0x81 => Some(ContentType::ReadReceipt),
0x82 => Some(ContentType::ContactCard),
0xFF => Some(ContentType::Custom),
_ => None,
}
}
pub fn is_text(&self) -> bool {
(*self as u8) >= 0x01 && (*self as u8) <= 0x0F
}
pub fn is_image(&self) -> bool {
(*self as u8) >= 0x30 && (*self as u8) <= 0x3F
}
}
impl Default for ContentType {
fn default() -> Self {
ContentType::TextPlain
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[repr(u8)]
pub enum InboxFilter {
#[default]
AcceptAll = 0,
ContactsOnly = 1,
StakedOnly = 2,
}
impl InboxFilter {
pub fn from_byte(b: u8) -> Option<Self> {
match b {
0 => Some(InboxFilter::AcceptAll),
1 => Some(InboxFilter::ContactsOnly),
2 => Some(InboxFilter::StakedOnly),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum AttachmentType {
Inline = 0x01,
External = 0x02,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum ExternalProtocol {
IPFS = 0x01,
Arweave = 0x02,
HTTPS = 0x03,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PendingPayment {
pub recipient_hash: [u8; 32],
pub amount: Balance,
pub expiry: u64,
pub sender: Address,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MessageEvent {
pub sender: Address,
pub recipient_hash: [u8; 32],
pub message_id: Hash,
pub size: u32,
pub has_payment: bool,
pub block_height: u64,
pub timestamp: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct QuotaInfo {
pub used_today: u32,
pub remaining: u32,
pub total_quota: u32,
pub tier: u8,
pub stake_amount: Balance,
pub resets_at: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SpamReport {
pub reporter: Address,
pub timestamp: u64,
pub message_id: Hash,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SendMessageData {
pub message_data: Vec<u8>,
pub recipient_hash: [u8; 32],
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SendMessageWithPaymentData {
pub message_data: Vec<u8>,
pub recipient_hash: [u8; 32],
pub koppa_amount: Balance,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClaimPaymentData {
pub message_id: Hash,
pub recipient_address: Address,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StakeForTrustData {
pub amount: Balance,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UnstakeData {
pub amount: Balance,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SetInboxFilterData {
pub mode: InboxFilter,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ContactData {
pub contact_hash: [u8; 32],
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BlockSenderData {
pub sender: Address,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReportSpamData {
pub message_id: Hash,
pub spammer: Address,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SetDailyQuotaData {
pub quota: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SetMaxMessageSizeData {
pub size: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SetMinTrustStakeData {
pub amount: Balance,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SetSponsorshipEnabledData {
pub enabled: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FundRegistryData {
pub amount: Balance,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RegisterPublicKeyData {
pub public_key: [u8; 32],
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UpdatePublicKeyData {
pub new_public_key: [u8; 32],
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RegisteredPublicKey {
pub public_key: [u8; 32],
pub address: Address,
pub registered_at_block: u64,
pub registered_at: u64,
pub updated_at_block: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SponsoredMessage {
pub message_data: Vec<u8>,
pub recipient_hash: [u8; 32],
#[serde(with = "BigArray")]
pub signature: [u8; 64],
pub sender_pubkey: [u8; 32],
pub nonce: u64,
pub expiry: u64,
pub koppa_amount: Option<Balance>,
}
impl SponsoredMessage {
pub fn signing_hash(&self, chain_id: u64, registry_address: &Address) -> Hash {
let mut domain_data = Vec::new();
domain_data.extend_from_slice(b"SRC-201-v1.1");
domain_data.extend_from_slice(&chain_id.to_be_bytes());
domain_data.extend_from_slice(registry_address.as_bytes());
let domain_separator = blake3::hash(&domain_data);
let message_hash = blake3::hash(&self.message_data);
let mut data = Vec::new();
data.extend_from_slice(domain_separator.as_bytes());
data.extend_from_slice(&self.sender_pubkey);
data.extend_from_slice(&self.recipient_hash);
data.extend_from_slice(message_hash.as_bytes());
data.extend_from_slice(&self.nonce.to_be_bytes());
data.extend_from_slice(&self.expiry.to_be_bytes());
if let Some(amount) = self.koppa_amount {
data.extend_from_slice(&amount.to_be_bytes());
}
Hash::hash(&data)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MessageHeader {
pub magic: [u8; 4],
pub version: u8,
pub flags: MessageFlags,
pub content_type: ContentType,
pub attachment_count: u8,
pub recipient_hash: [u8; 32],
pub ephemeral_pubkey: [u8; 32],
}
impl MessageHeader {
pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < SRC201_HEADER_SIZE {
return None;
}
let mut magic = [0u8; 4];
magic.copy_from_slice(&data[0..4]);
if magic != SRC201_MAGIC {
return None;
}
let version = data[4];
let flags = MessageFlags(data[5]);
let content_type = ContentType::from_byte(data[6])?;
let attachment_count = data[7];
let mut recipient_hash = [0u8; 32];
recipient_hash.copy_from_slice(&data[8..40]);
let mut ephemeral_pubkey = [0u8; 32];
ephemeral_pubkey.copy_from_slice(&data[40..72]);
Some(Self {
magic,
version,
flags,
content_type,
attachment_count,
recipient_hash,
ephemeral_pubkey,
})
}
pub fn to_bytes(&self) -> [u8; SRC201_HEADER_SIZE] {
let mut bytes = [0u8; SRC201_HEADER_SIZE];
bytes[0..4].copy_from_slice(&self.magic);
bytes[4] = self.version;
bytes[5] = self.flags.0;
bytes[6] = self.content_type as u8;
bytes[7] = self.attachment_count;
bytes[8..40].copy_from_slice(&self.recipient_hash);
bytes[40..72].copy_from_slice(&self.ephemeral_pubkey);
bytes
}
}
pub fn validate_message_format(data: &[u8]) -> Result<MessageHeader, &'static str> {
if data.len() < SRC201_HEADER_SIZE + SRC201_NONCE_SIZE + 2 + SRC201_TAG_SIZE {
return Err("Message too short");
}
let header = MessageHeader::from_bytes(data).ok_or("Invalid header")?;
if header.version != SRC201_VERSION {
return Err("Unsupported version");
}
let payload_len =
u16::from_be_bytes([data[SRC201_HEADER_SIZE + 24], data[SRC201_HEADER_SIZE + 25]]) as usize;
let expected_min_size = SRC201_HEADER_SIZE + SRC201_NONCE_SIZE + 2 + payload_len + SRC201_TAG_SIZE;
if data.len() < expected_min_size {
return Err("Payload length mismatch");
}
Ok(header)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_message_flags() {
let mut flags = MessageFlags::new();
assert!(!flags.is_encrypted());
flags.set(MessageFlags::ENCRYPTED);
assert!(flags.is_encrypted());
flags.set(MessageFlags::HAS_REPLY_TO);
assert!(flags.has_reply_to());
flags.clear(MessageFlags::ENCRYPTED);
assert!(!flags.is_encrypted());
assert!(flags.has_reply_to());
}
#[test]
fn test_messaging_operation_from_byte() {
assert_eq!(
MessagingOperation::from_byte(0),
Some(MessagingOperation::SendMessage)
);
assert_eq!(
MessagingOperation::from_byte(128),
Some(MessagingOperation::SetDailyQuota)
);
assert!(MessagingOperation::from_byte(200).is_none());
}
#[test]
fn test_content_type() {
assert!(ContentType::TextPlain.is_text());
assert!(ContentType::ImagePng.is_image());
assert!(!ContentType::ApplicationJson.is_text());
}
#[test]
fn test_message_header_roundtrip() {
let header = MessageHeader {
magic: SRC201_MAGIC,
version: SRC201_VERSION,
flags: MessageFlags::encrypted(),
content_type: ContentType::TextPlain,
attachment_count: 0,
recipient_hash: [1u8; 32],
ephemeral_pubkey: [2u8; 32],
};
let bytes = header.to_bytes();
let parsed = MessageHeader::from_bytes(&bytes).unwrap();
assert_eq!(header, parsed);
}
}