1use serde::{Deserialize, Serialize};
7use serde_big_array::BigArray;
8
9use crate::{Address, Balance, Hash};
10
11pub const SRC201_MAGIC: [u8; 4] = [0x53, 0x32, 0x30, 0x31];
13
14pub const SRC201_VERSION: u8 = 1;
16
17pub const SRC201_HEADER_SIZE: usize = 72;
19
20pub const SRC201_NONCE_SIZE: usize = 24;
22
23pub const SRC201_TAG_SIZE: usize = 16;
25
26pub const SRC201_KDF_CONTEXT: &str = "SRC-201-v1.1-message-key";
28
29pub const SRC201_ATTACHMENT_KDF_CONTEXT: &str = "SRC-201-v1.1-attachment-key";
31
32pub const DEFAULT_DAILY_QUOTA: u32 = 100;
34
35pub const DEFAULT_MAX_MESSAGE_SIZE: u32 = 65535;
37
38pub const DEFAULT_MIN_TRUST_STAKE: u128 = 100_000_000_000;
40
41pub const DEFAULT_SPAM_THRESHOLD: u32 = 50;
43
44pub const DEFAULT_HIGH_SPAM_THRESHOLD: u32 = 80;
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
49#[repr(u8)]
50pub enum MessagingOperation {
51 SendMessage = 0,
53 SendMessageDirect = 1,
55 SendMessageWithPayment = 2,
57 ClaimPayment = 3,
59 StakeForTrust = 4,
61 Unstake = 5,
63 SetInboxFilter = 6,
65 AddContact = 7,
67 RemoveContact = 8,
69 BlockSender = 9,
71 ReportSpam = 10,
73 RegisterPublicKey = 11,
75 UpdatePublicKey = 12,
77
78 SetDailyQuota = 128,
81 SetMaxMessageSize = 129,
83 SetMinTrustStake = 130,
85 SetSponsorshipEnabled = 131,
87 FundRegistry = 132,
89}
90
91impl MessagingOperation {
92 pub fn from_byte(b: u8) -> Option<Self> {
94 match b {
95 0 => Some(MessagingOperation::SendMessage),
96 1 => Some(MessagingOperation::SendMessageDirect),
97 2 => Some(MessagingOperation::SendMessageWithPayment),
98 3 => Some(MessagingOperation::ClaimPayment),
99 4 => Some(MessagingOperation::StakeForTrust),
100 5 => Some(MessagingOperation::Unstake),
101 6 => Some(MessagingOperation::SetInboxFilter),
102 7 => Some(MessagingOperation::AddContact),
103 8 => Some(MessagingOperation::RemoveContact),
104 9 => Some(MessagingOperation::BlockSender),
105 10 => Some(MessagingOperation::ReportSpam),
106 11 => Some(MessagingOperation::RegisterPublicKey),
107 12 => Some(MessagingOperation::UpdatePublicKey),
108 128 => Some(MessagingOperation::SetDailyQuota),
109 129 => Some(MessagingOperation::SetMaxMessageSize),
110 130 => Some(MessagingOperation::SetMinTrustStake),
111 131 => Some(MessagingOperation::SetSponsorshipEnabled),
112 132 => Some(MessagingOperation::FundRegistry),
113 _ => None,
114 }
115 }
116
117 pub fn is_admin(&self) -> bool {
119 (*self as u8) >= 128
120 }
121
122 pub fn is_send(&self) -> bool {
124 matches!(
125 self,
126 MessagingOperation::SendMessage
127 | MessagingOperation::SendMessageDirect
128 | MessagingOperation::SendMessageWithPayment
129 )
130 }
131}
132
133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
135pub struct MessagingTxData {
136 pub operation: MessagingOperation,
138 pub data: Vec<u8>,
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
144pub struct MessageFlags(pub u8);
145
146impl MessageFlags {
147 pub const ENCRYPTED: u8 = 1 << 0;
148 pub const HAS_REPLY_TO: u8 = 1 << 1;
149 pub const HAS_TIMESTAMP: u8 = 1 << 2;
150 pub const HAS_ATTACHMENTS: u8 = 1 << 3;
151 pub const IS_READ_RECEIPT: u8 = 1 << 4;
152 pub const IS_PAYMENT_REQUEST: u8 = 1 << 5;
153 pub const REQUIRES_STAKE: u8 = 1 << 6;
154
155 pub fn new() -> Self {
156 Self(0)
157 }
158
159 pub fn encrypted() -> Self {
160 Self(Self::ENCRYPTED)
161 }
162
163 pub fn is_encrypted(&self) -> bool {
164 self.0 & Self::ENCRYPTED != 0
165 }
166
167 pub fn has_reply_to(&self) -> bool {
168 self.0 & Self::HAS_REPLY_TO != 0
169 }
170
171 pub fn has_timestamp(&self) -> bool {
172 self.0 & Self::HAS_TIMESTAMP != 0
173 }
174
175 pub fn has_attachments(&self) -> bool {
176 self.0 & Self::HAS_ATTACHMENTS != 0
177 }
178
179 pub fn is_read_receipt(&self) -> bool {
180 self.0 & Self::IS_READ_RECEIPT != 0
181 }
182
183 pub fn is_payment_request(&self) -> bool {
184 self.0 & Self::IS_PAYMENT_REQUEST != 0
185 }
186
187 pub fn requires_stake(&self) -> bool {
188 self.0 & Self::REQUIRES_STAKE != 0
189 }
190
191 pub fn set(&mut self, flag: u8) {
192 self.0 |= flag;
193 }
194
195 pub fn clear(&mut self, flag: u8) {
196 self.0 &= !flag;
197 }
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
202#[repr(u8)]
203pub enum ContentType {
204 TextPlain = 0x01,
206 TextMarkdown = 0x02,
207 TextHtml = 0x03,
208
209 ApplicationJson = 0x10,
211 ApplicationPdf = 0x11,
212
213 ImagePng = 0x30,
215 ImageJpeg = 0x31,
216 ImageGif = 0x32,
217 ImageWebp = 0x33,
218
219 PaymentRequest = 0x80,
221 ReadReceipt = 0x81,
222 ContactCard = 0x82,
223
224 Custom = 0xFF,
226}
227
228impl ContentType {
229 pub fn from_byte(b: u8) -> Option<Self> {
230 match b {
231 0x01 => Some(ContentType::TextPlain),
232 0x02 => Some(ContentType::TextMarkdown),
233 0x03 => Some(ContentType::TextHtml),
234 0x10 => Some(ContentType::ApplicationJson),
235 0x11 => Some(ContentType::ApplicationPdf),
236 0x30 => Some(ContentType::ImagePng),
237 0x31 => Some(ContentType::ImageJpeg),
238 0x32 => Some(ContentType::ImageGif),
239 0x33 => Some(ContentType::ImageWebp),
240 0x80 => Some(ContentType::PaymentRequest),
241 0x81 => Some(ContentType::ReadReceipt),
242 0x82 => Some(ContentType::ContactCard),
243 0xFF => Some(ContentType::Custom),
244 _ => None,
245 }
246 }
247
248 pub fn is_text(&self) -> bool {
249 (*self as u8) >= 0x01 && (*self as u8) <= 0x0F
250 }
251
252 pub fn is_image(&self) -> bool {
253 (*self as u8) >= 0x30 && (*self as u8) <= 0x3F
254 }
255}
256
257impl Default for ContentType {
258 fn default() -> Self {
259 ContentType::TextPlain
260 }
261}
262
263#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
265#[repr(u8)]
266pub enum InboxFilter {
267 #[default]
269 AcceptAll = 0,
270 ContactsOnly = 1,
272 StakedOnly = 2,
274}
275
276impl InboxFilter {
277 pub fn from_byte(b: u8) -> Option<Self> {
278 match b {
279 0 => Some(InboxFilter::AcceptAll),
280 1 => Some(InboxFilter::ContactsOnly),
281 2 => Some(InboxFilter::StakedOnly),
282 _ => None,
283 }
284 }
285}
286
287#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
289#[repr(u8)]
290pub enum AttachmentType {
291 Inline = 0x01,
293 External = 0x02,
295}
296
297#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
299#[repr(u8)]
300pub enum ExternalProtocol {
301 IPFS = 0x01,
302 Arweave = 0x02,
303 HTTPS = 0x03,
304}
305
306#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
308pub struct PendingPayment {
309 pub recipient_hash: [u8; 32],
311 pub amount: Balance,
313 pub expiry: u64,
315 pub sender: Address,
317}
318
319#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
321pub struct MessageEvent {
322 pub sender: Address,
324 pub recipient_hash: [u8; 32],
326 pub message_id: Hash,
328 pub size: u32,
330 pub has_payment: bool,
332 pub block_height: u64,
334 pub timestamp: u64,
336}
337
338#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
340pub struct QuotaInfo {
341 pub used_today: u32,
343 pub remaining: u32,
345 pub total_quota: u32,
347 pub tier: u8,
349 pub stake_amount: Balance,
351 pub resets_at: u64,
353}
354
355#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
357pub struct SpamReport {
358 pub reporter: Address,
360 pub timestamp: u64,
362 pub message_id: Hash,
364}
365
366#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
372pub struct SendMessageData {
373 pub message_data: Vec<u8>,
375 pub recipient_hash: [u8; 32],
377}
378
379#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
381pub struct SendMessageWithPaymentData {
382 pub message_data: Vec<u8>,
384 pub recipient_hash: [u8; 32],
386 pub koppa_amount: Balance,
388}
389
390#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
392pub struct ClaimPaymentData {
393 pub message_id: Hash,
395 pub recipient_address: Address,
397}
398
399#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
401pub struct StakeForTrustData {
402 pub amount: Balance,
404}
405
406#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
408pub struct UnstakeData {
409 pub amount: Balance,
411}
412
413#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
415pub struct SetInboxFilterData {
416 pub mode: InboxFilter,
418}
419
420#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
422pub struct ContactData {
423 pub contact_hash: [u8; 32],
425}
426
427#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
429pub struct BlockSenderData {
430 pub sender: Address,
432}
433
434#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
436pub struct ReportSpamData {
437 pub message_id: Hash,
439 pub spammer: Address,
441}
442
443#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
445pub struct SetDailyQuotaData {
446 pub quota: u32,
447}
448
449#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
451pub struct SetMaxMessageSizeData {
452 pub size: u32,
453}
454
455#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
457pub struct SetMinTrustStakeData {
458 pub amount: Balance,
459}
460
461#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
463pub struct SetSponsorshipEnabledData {
464 pub enabled: bool,
465}
466
467#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
469pub struct FundRegistryData {
470 pub amount: Balance,
471}
472
473#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
475pub struct RegisterPublicKeyData {
476 pub public_key: [u8; 32],
478}
479
480#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
482pub struct UpdatePublicKeyData {
483 pub new_public_key: [u8; 32],
485}
486
487#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
489pub struct RegisteredPublicKey {
490 pub public_key: [u8; 32],
492 pub address: Address,
494 pub registered_at_block: u64,
496 pub registered_at: u64,
498 pub updated_at_block: u64,
500}
501
502#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
508pub struct SponsoredMessage {
509 pub message_data: Vec<u8>,
511 pub recipient_hash: [u8; 32],
513 #[serde(with = "BigArray")]
515 pub signature: [u8; 64],
516 pub sender_pubkey: [u8; 32],
518 pub nonce: u64,
520 pub expiry: u64,
522 pub koppa_amount: Option<Balance>,
524}
525
526impl SponsoredMessage {
527 pub fn signing_hash(&self, chain_id: u64, registry_address: &Address) -> Hash {
529 let mut domain_data = Vec::new();
531 domain_data.extend_from_slice(b"SRC-201-v1.1");
532 domain_data.extend_from_slice(&chain_id.to_be_bytes());
533 domain_data.extend_from_slice(registry_address.as_bytes());
534 let domain_separator = blake3::hash(&domain_data);
535
536 let message_hash = blake3::hash(&self.message_data);
538
539 let mut data = Vec::new();
540 data.extend_from_slice(domain_separator.as_bytes());
541 data.extend_from_slice(&self.sender_pubkey);
542 data.extend_from_slice(&self.recipient_hash);
543 data.extend_from_slice(message_hash.as_bytes());
544 data.extend_from_slice(&self.nonce.to_be_bytes());
545 data.extend_from_slice(&self.expiry.to_be_bytes());
546 if let Some(amount) = self.koppa_amount {
547 data.extend_from_slice(&amount.to_be_bytes());
548 }
549
550 Hash::hash(&data)
551 }
552}
553
554#[derive(Debug, Clone, PartialEq, Eq)]
560pub struct MessageHeader {
561 pub magic: [u8; 4],
562 pub version: u8,
563 pub flags: MessageFlags,
564 pub content_type: ContentType,
565 pub attachment_count: u8,
566 pub recipient_hash: [u8; 32],
567 pub ephemeral_pubkey: [u8; 32],
568}
569
570impl MessageHeader {
571 pub fn from_bytes(data: &[u8]) -> Option<Self> {
573 if data.len() < SRC201_HEADER_SIZE {
574 return None;
575 }
576
577 let mut magic = [0u8; 4];
578 magic.copy_from_slice(&data[0..4]);
579
580 if magic != SRC201_MAGIC {
581 return None;
582 }
583
584 let version = data[4];
585 let flags = MessageFlags(data[5]);
586 let content_type = ContentType::from_byte(data[6])?;
587 let attachment_count = data[7];
588
589 let mut recipient_hash = [0u8; 32];
590 recipient_hash.copy_from_slice(&data[8..40]);
591
592 let mut ephemeral_pubkey = [0u8; 32];
593 ephemeral_pubkey.copy_from_slice(&data[40..72]);
594
595 Some(Self {
596 magic,
597 version,
598 flags,
599 content_type,
600 attachment_count,
601 recipient_hash,
602 ephemeral_pubkey,
603 })
604 }
605
606 pub fn to_bytes(&self) -> [u8; SRC201_HEADER_SIZE] {
608 let mut bytes = [0u8; SRC201_HEADER_SIZE];
609 bytes[0..4].copy_from_slice(&self.magic);
610 bytes[4] = self.version;
611 bytes[5] = self.flags.0;
612 bytes[6] = self.content_type as u8;
613 bytes[7] = self.attachment_count;
614 bytes[8..40].copy_from_slice(&self.recipient_hash);
615 bytes[40..72].copy_from_slice(&self.ephemeral_pubkey);
616 bytes
617 }
618}
619
620pub fn validate_message_format(data: &[u8]) -> Result<MessageHeader, &'static str> {
622 if data.len() < SRC201_HEADER_SIZE + SRC201_NONCE_SIZE + 2 + SRC201_TAG_SIZE {
623 return Err("Message too short");
624 }
625
626 let header = MessageHeader::from_bytes(data).ok_or("Invalid header")?;
627
628 if header.version != SRC201_VERSION {
629 return Err("Unsupported version");
630 }
631
632 let payload_len =
634 u16::from_be_bytes([data[SRC201_HEADER_SIZE + 24], data[SRC201_HEADER_SIZE + 25]]) as usize;
635
636 let expected_min_size = SRC201_HEADER_SIZE + SRC201_NONCE_SIZE + 2 + payload_len + SRC201_TAG_SIZE;
637 if data.len() < expected_min_size {
638 return Err("Payload length mismatch");
639 }
640
641 Ok(header)
642}
643
644#[cfg(test)]
645mod tests {
646 use super::*;
647
648 #[test]
649 fn test_message_flags() {
650 let mut flags = MessageFlags::new();
651 assert!(!flags.is_encrypted());
652
653 flags.set(MessageFlags::ENCRYPTED);
654 assert!(flags.is_encrypted());
655
656 flags.set(MessageFlags::HAS_REPLY_TO);
657 assert!(flags.has_reply_to());
658
659 flags.clear(MessageFlags::ENCRYPTED);
660 assert!(!flags.is_encrypted());
661 assert!(flags.has_reply_to());
662 }
663
664 #[test]
665 fn test_messaging_operation_from_byte() {
666 assert_eq!(
667 MessagingOperation::from_byte(0),
668 Some(MessagingOperation::SendMessage)
669 );
670 assert_eq!(
671 MessagingOperation::from_byte(128),
672 Some(MessagingOperation::SetDailyQuota)
673 );
674 assert!(MessagingOperation::from_byte(200).is_none());
675 }
676
677 #[test]
678 fn test_content_type() {
679 assert!(ContentType::TextPlain.is_text());
680 assert!(ContentType::ImagePng.is_image());
681 assert!(!ContentType::ApplicationJson.is_text());
682 }
683
684 #[test]
685 fn test_message_header_roundtrip() {
686 let header = MessageHeader {
687 magic: SRC201_MAGIC,
688 version: SRC201_VERSION,
689 flags: MessageFlags::encrypted(),
690 content_type: ContentType::TextPlain,
691 attachment_count: 0,
692 recipient_hash: [1u8; 32],
693 ephemeral_pubkey: [2u8; 32],
694 };
695
696 let bytes = header.to_bytes();
697 let parsed = MessageHeader::from_bytes(&bytes).unwrap();
698
699 assert_eq!(header, parsed);
700 }
701}