1use crate::{error::Result, TallyError};
4use anchor_client::solana_sdk::{signature::Signature, transaction::TransactionError};
5use anchor_lang::prelude::*;
6use base64::prelude::*;
7use chrono;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11#[derive(
13 Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
14)]
15pub struct Subscribed {
16 pub merchant: Pubkey,
18 pub plan: Pubkey,
20 pub subscriber: Pubkey,
22 pub amount: u64,
24}
25
26#[derive(
28 Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
29)]
30pub struct Renewed {
31 pub merchant: Pubkey,
33 pub plan: Pubkey,
35 pub subscriber: Pubkey,
37 pub amount: u64,
39}
40
41#[derive(
43 Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
44)]
45pub struct Canceled {
46 pub merchant: Pubkey,
48 pub plan: Pubkey,
50 pub subscriber: Pubkey,
52}
53
54#[derive(
56 Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
57)]
58pub struct PaymentFailed {
59 pub merchant: Pubkey,
61 pub plan: Pubkey,
63 pub subscriber: Pubkey,
65 pub reason: String,
67}
68
69#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
71pub enum TallyEvent {
72 Subscribed(Subscribed),
74 Renewed(Renewed),
76 Canceled(Canceled),
78 PaymentFailed(PaymentFailed),
80}
81
82#[derive(Clone, Debug, Serialize, Deserialize)]
84pub struct ParsedEventWithContext {
85 pub signature: Signature,
87 pub slot: u64,
89 pub block_time: Option<i64>,
91 pub success: bool,
93 pub event: TallyEvent,
95 pub log_index: usize,
97}
98
99#[derive(Clone, Debug, Serialize, Deserialize)]
101pub struct StreamableEventData {
102 pub event_type: String,
104 pub merchant_pda: String,
106 pub transaction_signature: String,
108 pub timestamp: i64,
110 pub metadata: HashMap<String, String>,
112 pub amount: Option<u64>,
114 pub plan_address: Option<String>,
116 pub subscription_address: Option<String>,
118}
119
120impl ParsedEventWithContext {
121 #[must_use]
123 pub const fn new(
124 signature: Signature,
125 slot: u64,
126 block_time: Option<i64>,
127 success: bool,
128 event: TallyEvent,
129 log_index: usize,
130 ) -> Self {
131 Self {
132 signature,
133 slot,
134 block_time,
135 success,
136 event,
137 log_index,
138 }
139 }
140
141 #[must_use]
143 pub fn to_streamable(&self) -> StreamableEventData {
144 let (event_type, merchant_pda, plan_address, subscriber, amount, reason) = match &self.event
145 {
146 TallyEvent::Subscribed(e) => (
147 "subscribed".to_string(),
148 e.merchant.to_string(),
149 Some(e.plan.to_string()),
150 Some(e.subscriber.to_string()),
151 Some(e.amount),
152 None,
153 ),
154 TallyEvent::Renewed(e) => (
155 "renewed".to_string(),
156 e.merchant.to_string(),
157 Some(e.plan.to_string()),
158 Some(e.subscriber.to_string()),
159 Some(e.amount),
160 None,
161 ),
162 TallyEvent::Canceled(e) => (
163 "canceled".to_string(),
164 e.merchant.to_string(),
165 Some(e.plan.to_string()),
166 Some(e.subscriber.to_string()),
167 None,
168 None,
169 ),
170 TallyEvent::PaymentFailed(e) => (
171 "payment_failed".to_string(),
172 e.merchant.to_string(),
173 Some(e.plan.to_string()),
174 Some(e.subscriber.to_string()),
175 None,
176 Some(e.reason.clone()),
177 ),
178 };
179
180 let mut metadata = HashMap::new();
181 if let Some(subscriber) = subscriber {
182 metadata.insert("subscriber".to_string(), subscriber);
183 }
184 if let Some(reason) = reason {
185 metadata.insert("reason".to_string(), reason);
186 }
187 metadata.insert("slot".to_string(), self.slot.to_string());
188 metadata.insert("success".to_string(), self.success.to_string());
189
190 let subscription_address = if plan_address.is_some() && metadata.contains_key("subscriber")
192 {
193 let subscriber_str = metadata.get("subscriber").map_or("unknown", String::as_str);
194 Some(format!(
195 "subscription_{}_{}",
196 plan_address.as_deref().unwrap_or("unknown"),
197 subscriber_str
198 ))
199 } else {
200 None
201 };
202
203 StreamableEventData {
204 event_type,
205 merchant_pda,
206 transaction_signature: self.signature.to_string(),
207 timestamp: self.block_time.unwrap_or(0),
208 metadata,
209 amount,
210 plan_address,
211 subscription_address,
212 }
213 }
214
215 #[must_use]
217 pub const fn is_successful(&self) -> bool {
218 self.success
219 }
220
221 #[must_use]
223 pub const fn get_merchant(&self) -> Option<Pubkey> {
224 match &self.event {
225 TallyEvent::Subscribed(e) => Some(e.merchant),
226 TallyEvent::Renewed(e) => Some(e.merchant),
227 TallyEvent::Canceled(e) => Some(e.merchant),
228 TallyEvent::PaymentFailed(e) => Some(e.merchant),
229 }
230 }
231
232 #[must_use]
234 pub const fn get_plan(&self) -> Option<Pubkey> {
235 match &self.event {
236 TallyEvent::Subscribed(e) => Some(e.plan),
237 TallyEvent::Renewed(e) => Some(e.plan),
238 TallyEvent::Canceled(e) => Some(e.plan),
239 TallyEvent::PaymentFailed(e) => Some(e.plan),
240 }
241 }
242
243 #[must_use]
245 pub const fn get_subscriber(&self) -> Option<Pubkey> {
246 match &self.event {
247 TallyEvent::Subscribed(e) => Some(e.subscriber),
248 TallyEvent::Renewed(e) => Some(e.subscriber),
249 TallyEvent::Canceled(e) => Some(e.subscriber),
250 TallyEvent::PaymentFailed(e) => Some(e.subscriber),
251 }
252 }
253
254 #[must_use]
256 pub const fn get_amount(&self) -> Option<u64> {
257 match &self.event {
258 TallyEvent::Subscribed(e) => Some(e.amount),
259 TallyEvent::Renewed(e) => Some(e.amount),
260 TallyEvent::Canceled(_) | TallyEvent::PaymentFailed(_) => None,
261 }
262 }
263
264 #[must_use]
266 pub fn get_event_type_string(&self) -> String {
267 match &self.event {
268 TallyEvent::Subscribed(_) => "Subscribed".to_string(),
269 TallyEvent::Renewed(_) => "Renewed".to_string(),
270 TallyEvent::Canceled(_) => "Canceled".to_string(),
271 TallyEvent::PaymentFailed(_) => "PaymentFailed".to_string(),
272 }
273 }
274
275 #[must_use]
277 #[allow(clippy::cast_precision_loss)]
278 pub fn format_amount(&self) -> Option<f64> {
279 self.get_amount().map(|amount| amount as f64 / 1_000_000.0)
280 }
281
282 #[must_use]
284 pub fn format_timestamp(&self) -> String {
285 self.block_time.map_or_else(
286 || "Pending".to_string(),
287 |timestamp| {
288 chrono::DateTime::from_timestamp(timestamp, 0)
289 .map_or_else(|| "Unknown".to_string(), |dt| dt.to_rfc3339())
290 },
291 )
292 }
293
294 #[must_use]
296 pub const fn affects_revenue(&self) -> bool {
297 matches!(
298 &self.event,
299 TallyEvent::Subscribed(_) | TallyEvent::Renewed(_)
300 )
301 }
302
303 #[must_use]
305 pub const fn affects_subscription_count(&self) -> bool {
306 matches!(
307 &self.event,
308 TallyEvent::Subscribed(_) | TallyEvent::Canceled(_)
309 )
310 }
311
312 #[must_use]
314 pub fn get_failure_reason(&self) -> Option<&str> {
315 match &self.event {
316 TallyEvent::PaymentFailed(e) => Some(&e.reason),
317 _ => None,
318 }
319 }
320}
321
322fn compute_event_discriminator(event_name: &str) -> [u8; 8] {
325 use anchor_lang::solana_program::hash;
326 let preimage = format!("event:{event_name}");
327 let hash_result = hash::hash(preimage.as_bytes());
328 let mut discriminator = [0u8; 8];
329 discriminator.copy_from_slice(&hash_result.to_bytes()[..8]);
330 discriminator
331}
332
333fn get_event_discriminators() -> HashMap<[u8; 8], &'static str> {
335 let mut discriminators = HashMap::new();
336 discriminators.insert(compute_event_discriminator("Subscribed"), "Subscribed");
337 discriminators.insert(compute_event_discriminator("Renewed"), "Renewed");
338 discriminators.insert(compute_event_discriminator("Canceled"), "Canceled");
339 discriminators.insert(
340 compute_event_discriminator("PaymentFailed"),
341 "PaymentFailed",
342 );
343 discriminators
344}
345
346#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
348pub struct TallyReceipt {
349 pub signature: Signature,
351 pub block_time: Option<i64>,
353 pub slot: u64,
355 pub success: bool,
357 pub error: Option<String>,
359 pub events: Vec<TallyEvent>,
361 pub logs: Vec<String>,
363 pub compute_units_consumed: Option<u64>,
365 pub fee: u64,
367}
368
369pub fn parse_events_with_context(
383 logs: &[String],
384 program_id: &Pubkey,
385 signature: Signature,
386 slot: u64,
387 block_time: Option<i64>,
388 success: bool,
389) -> Result<Vec<ParsedEventWithContext>> {
390 let events = parse_events_from_logs(logs, program_id)?;
391 let mut parsed_events = Vec::new();
392
393 for (log_index, event) in events.into_iter().enumerate() {
394 parsed_events.push(ParsedEventWithContext::new(
395 signature, slot, block_time, success, event, log_index,
396 ));
397 }
398
399 Ok(parsed_events)
400}
401
402pub fn parse_events_from_logs(logs: &[String], program_id: &Pubkey) -> Result<Vec<TallyEvent>> {
412 let mut events = Vec::new();
413 let program_data_prefix = format!("Program data: {program_id} ");
414
415 for log in logs {
416 if let Some(data_start) = log.find(&program_data_prefix) {
417 let event_data = &log[data_start.saturating_add(program_data_prefix.len())..];
418 if let Ok(event) = parse_single_event(event_data) {
419 events.push(event);
420 }
421 }
422 }
423
424 Ok(events)
425}
426
427pub fn parse_single_event(data: &str) -> Result<TallyEvent> {
432 let decoded_data = base64::prelude::BASE64_STANDARD
434 .decode(data)
435 .map_err(|e| TallyError::ParseError(format!("Failed to decode base64: {e}")))?;
436
437 if decoded_data.len() < 8 {
439 return Err(TallyError::ParseError(
440 "Event data too short, must be at least 8 bytes for discriminator".to_string(),
441 ));
442 }
443
444 let mut discriminator = [0u8; 8];
446 discriminator.copy_from_slice(&decoded_data[..8]);
447
448 let event_data = &decoded_data[8..];
450
451 let discriminators = get_event_discriminators();
453 let event_type = discriminators.get(&discriminator).ok_or_else(|| {
454 TallyError::ParseError(format!("Unknown event discriminator: {discriminator:?}"))
455 })?;
456
457 match *event_type {
459 "Subscribed" => {
460 let event = Subscribed::try_from_slice(event_data).map_err(|e| {
461 TallyError::ParseError(format!("Failed to deserialize Subscribed event: {e}"))
462 })?;
463 Ok(TallyEvent::Subscribed(event))
464 }
465 "Renewed" => {
466 let event = Renewed::try_from_slice(event_data).map_err(|e| {
467 TallyError::ParseError(format!("Failed to deserialize Renewed event: {e}"))
468 })?;
469 Ok(TallyEvent::Renewed(event))
470 }
471 "Canceled" => {
472 let event = Canceled::try_from_slice(event_data).map_err(|e| {
473 TallyError::ParseError(format!("Failed to deserialize Canceled event: {e}"))
474 })?;
475 Ok(TallyEvent::Canceled(event))
476 }
477 "PaymentFailed" => {
478 let event = PaymentFailed::try_from_slice(event_data).map_err(|e| {
479 TallyError::ParseError(format!("Failed to deserialize PaymentFailed event: {e}"))
480 })?;
481 Ok(TallyEvent::PaymentFailed(event))
482 }
483 _ => Err(TallyError::ParseError(format!(
484 "Unhandled event type: {event_type}"
485 ))),
486 }
487}
488
489pub struct ReceiptParams {
491 pub signature: Signature,
493 pub block_time: Option<i64>,
495 pub slot: u64,
497 pub success: bool,
499 pub error: Option<TransactionError>,
501 pub logs: Vec<String>,
503 pub compute_units_consumed: Option<u64>,
505 pub fee: u64,
507 pub program_id: Pubkey,
509}
510
511pub fn create_receipt(params: ReceiptParams) -> Result<TallyReceipt> {
520 let events = parse_events_from_logs(¶ms.logs, ¶ms.program_id)?;
521
522 Ok(TallyReceipt {
523 signature: params.signature,
524 block_time: params.block_time,
525 slot: params.slot,
526 success: params.success,
527 error: params.error.map(|e| format!("{e:?}")),
528 events,
529 logs: params.logs,
530 compute_units_consumed: params.compute_units_consumed,
531 fee: params.fee,
532 })
533}
534
535#[allow(clippy::too_many_arguments)] pub fn create_receipt_legacy(
553 signature: Signature,
554 block_time: Option<i64>,
555 slot: u64,
556 success: bool,
557 error: Option<TransactionError>,
558 logs: Vec<String>,
559 compute_units_consumed: Option<u64>,
560 fee: u64,
561 program_id: &Pubkey,
562) -> Result<TallyReceipt> {
563 create_receipt(ReceiptParams {
564 signature,
565 block_time,
566 slot,
567 success,
568 error,
569 logs,
570 compute_units_consumed,
571 fee,
572 program_id: *program_id,
573 })
574}
575
576#[must_use]
584pub fn extract_memo_from_logs(logs: &[String]) -> Option<String> {
585 for log in logs {
586 if log.starts_with("Program log: Memo (len ") {
587 if let Some(start) = log.find("): \"") {
589 let memo_start = start.saturating_add(4); if let Some(end) = log.rfind('"') {
591 if end > memo_start {
592 return Some(log[memo_start..end].to_string());
593 }
594 }
595 }
596 } else if log.starts_with("Program log: ") && log.contains("memo:") {
597 if let Some(memo_start) = log.find("memo:") {
599 let memo_content = &log[memo_start.saturating_add(5)..].trim();
600 return Some((*memo_content).to_string());
601 }
602 }
603 }
604 None
605}
606
607impl TallyReceipt {
609 #[must_use]
611 pub fn get_subscribed_event(&self) -> Option<&Subscribed> {
612 self.events.iter().find_map(|event| match event {
613 TallyEvent::Subscribed(e) => Some(e),
614 _ => None,
615 })
616 }
617
618 #[must_use]
620 pub fn get_renewed_event(&self) -> Option<&Renewed> {
621 self.events.iter().find_map(|event| match event {
622 TallyEvent::Renewed(e) => Some(e),
623 _ => None,
624 })
625 }
626
627 #[must_use]
629 pub fn get_canceled_event(&self) -> Option<&Canceled> {
630 self.events.iter().find_map(|event| match event {
631 TallyEvent::Canceled(e) => Some(e),
632 _ => None,
633 })
634 }
635
636 #[must_use]
638 pub fn get_payment_failed_event(&self) -> Option<&PaymentFailed> {
639 self.events.iter().find_map(|event| match event {
640 TallyEvent::PaymentFailed(e) => Some(e),
641 _ => None,
642 })
643 }
644
645 #[must_use]
647 pub fn extract_memo(&self) -> Option<String> {
648 extract_memo_from_logs(&self.logs)
649 }
650
651 #[must_use]
653 pub fn is_subscription_success(&self) -> bool {
654 self.success
655 && (self.get_subscribed_event().is_some()
656 || self.get_renewed_event().is_some()
657 || self.get_canceled_event().is_some())
658 }
659}
660
661#[cfg(test)]
662mod tests {
663 use super::*;
664 use anchor_client::solana_sdk::signature::{Keypair, Signer};
665
666 #[test]
667 fn test_extract_memo_from_logs() {
668 let logs = vec![
669 "Program 11111111111111111111111111111111 invoke [1]".to_string(),
670 "Program log: Memo (len 12): \"Test message\"".to_string(),
671 "Program 11111111111111111111111111111111 consumed 1000 of 200000 compute units"
672 .to_string(),
673 ];
674
675 let memo = extract_memo_from_logs(&logs);
676 assert_eq!(memo, Some("Test message".to_string()));
677 }
678
679 #[test]
680 fn test_extract_memo_alternative_format() {
681 let logs = vec!["Program log: Processing memo: Hello world".to_string()];
682
683 let memo = extract_memo_from_logs(&logs);
684 assert_eq!(memo, Some("Hello world".to_string()));
685 }
686
687 #[test]
688 fn test_extract_memo_none() {
689 let logs = vec![
690 "Program 11111111111111111111111111111111 invoke [1]".to_string(),
691 "Program 11111111111111111111111111111111 consumed 1000 of 200000 compute units"
692 .to_string(),
693 ];
694
695 let memo = extract_memo_from_logs(&logs);
696 assert_eq!(memo, None);
697 }
698
699 #[test]
700 fn test_tally_receipt_event_getters() {
701 let signature = Signature::default();
702 let merchant = Pubkey::from(Keypair::new().pubkey().to_bytes());
703 let plan = Pubkey::from(Keypair::new().pubkey().to_bytes());
704 let subscriber = Pubkey::from(Keypair::new().pubkey().to_bytes());
705
706 let subscribed_event = Subscribed {
707 merchant,
708 plan,
709 subscriber,
710 amount: 1_000_000, };
712
713 let receipt = TallyReceipt {
714 signature,
715 block_time: Some(1_640_995_200), slot: 100,
717 success: true,
718 error: None,
719 events: vec![TallyEvent::Subscribed(subscribed_event.clone())],
720 logs: vec![],
721 compute_units_consumed: Some(5000),
722 fee: 5000,
723 };
724
725 assert_eq!(receipt.get_subscribed_event(), Some(&subscribed_event));
726 assert_eq!(receipt.get_renewed_event(), None);
727 assert_eq!(receipt.get_canceled_event(), None);
728 assert_eq!(receipt.get_payment_failed_event(), None);
729 assert!(receipt.is_subscription_success());
730 }
731
732 #[test]
733 fn test_tally_receipt_failed_transaction() {
734 let signature = Signature::default();
735
736 let receipt = TallyReceipt {
737 signature,
738 block_time: Some(1_640_995_200),
739 slot: 100,
740 success: false,
741 error: Some("InsufficientFunds".to_string()),
742 events: vec![],
743 logs: vec![],
744 compute_units_consumed: Some(1000),
745 fee: 5000,
746 };
747
748 assert!(!receipt.is_subscription_success());
749 }
750
751 #[test]
752 fn test_create_receipt() {
753 let signature = Signature::default();
754 let program_id = crate::program_id();
755
756 let receipt = create_receipt(ReceiptParams {
757 signature,
758 block_time: Some(1_640_995_200),
759 slot: 100,
760 success: true,
761 error: None,
762 logs: vec!["Program invoked".to_string()],
763 compute_units_consumed: Some(5000),
764 fee: 5000,
765 program_id,
766 })
767 .unwrap();
768
769 assert_eq!(receipt.signature, signature);
770 assert_eq!(receipt.slot, 100);
771 assert!(receipt.success);
772 assert_eq!(receipt.error, None);
773 assert_eq!(receipt.fee, 5000);
774 }
775
776 fn create_test_event_data(event_name: &str, event_struct: &impl AnchorSerialize) -> String {
778 let discriminator = compute_event_discriminator(event_name);
779 let mut event_data = Vec::new();
780 event_data.extend_from_slice(&discriminator);
781 event_struct.serialize(&mut event_data).unwrap();
782 base64::prelude::BASE64_STANDARD.encode(event_data)
783 }
784
785 #[test]
786 fn test_compute_event_discriminator() {
787 let subscribed_disc = compute_event_discriminator("Subscribed");
788 let renewed_disc = compute_event_discriminator("Renewed");
789 let canceled_disc = compute_event_discriminator("Canceled");
790 let payment_failed_disc = compute_event_discriminator("PaymentFailed");
791
792 assert_ne!(subscribed_disc, renewed_disc);
794 assert_ne!(subscribed_disc, canceled_disc);
795 assert_ne!(subscribed_disc, payment_failed_disc);
796 assert_ne!(renewed_disc, canceled_disc);
797 assert_ne!(renewed_disc, payment_failed_disc);
798 assert_ne!(canceled_disc, payment_failed_disc);
799
800 assert_eq!(subscribed_disc, compute_event_discriminator("Subscribed"));
802 assert_eq!(renewed_disc, compute_event_discriminator("Renewed"));
803 }
804
805 #[test]
806 fn test_get_event_discriminators() {
807 let discriminators = get_event_discriminators();
808
809 assert_eq!(discriminators.len(), 4);
810 assert!(discriminators.contains_key(&compute_event_discriminator("Subscribed")));
811 assert!(discriminators.contains_key(&compute_event_discriminator("Renewed")));
812 assert!(discriminators.contains_key(&compute_event_discriminator("Canceled")));
813 assert!(discriminators.contains_key(&compute_event_discriminator("PaymentFailed")));
814 }
815
816 #[test]
817 fn test_parse_subscribed_event() {
818 let merchant = Pubkey::from(Keypair::new().pubkey().to_bytes());
819 let plan = Pubkey::from(Keypair::new().pubkey().to_bytes());
820 let subscriber = Pubkey::from(Keypair::new().pubkey().to_bytes());
821
822 let event = Subscribed {
823 merchant,
824 plan,
825 subscriber,
826 amount: 5_000_000, };
828
829 let encoded_data = create_test_event_data("Subscribed", &event);
830 let parsed_event = parse_single_event(&encoded_data).unwrap();
831
832 match parsed_event {
833 TallyEvent::Subscribed(parsed) => {
834 assert_eq!(parsed.merchant, merchant);
835 assert_eq!(parsed.plan, plan);
836 assert_eq!(parsed.subscriber, subscriber);
837 assert_eq!(parsed.amount, 5_000_000);
838 }
839 _ => panic!("Expected Subscribed event"),
840 }
841 }
842
843 #[test]
844 fn test_parse_renewed_event() {
845 let merchant = Pubkey::from(Keypair::new().pubkey().to_bytes());
846 let plan = Pubkey::from(Keypair::new().pubkey().to_bytes());
847 let subscriber = Pubkey::from(Keypair::new().pubkey().to_bytes());
848
849 let event = Renewed {
850 merchant,
851 plan,
852 subscriber,
853 amount: 10_000_000, };
855
856 let encoded_data = create_test_event_data("Renewed", &event);
857 let parsed_event = parse_single_event(&encoded_data).unwrap();
858
859 match parsed_event {
860 TallyEvent::Renewed(parsed) => {
861 assert_eq!(parsed.merchant, merchant);
862 assert_eq!(parsed.plan, plan);
863 assert_eq!(parsed.subscriber, subscriber);
864 assert_eq!(parsed.amount, 10_000_000);
865 }
866 _ => panic!("Expected Renewed event"),
867 }
868 }
869
870 #[test]
871 fn test_parse_canceled_event() {
872 let merchant = Pubkey::from(Keypair::new().pubkey().to_bytes());
873 let plan = Pubkey::from(Keypair::new().pubkey().to_bytes());
874 let subscriber = Pubkey::from(Keypair::new().pubkey().to_bytes());
875
876 let event = Canceled {
877 merchant,
878 plan,
879 subscriber,
880 };
881
882 let encoded_data = create_test_event_data("Canceled", &event);
883 let parsed_event = parse_single_event(&encoded_data).unwrap();
884
885 match parsed_event {
886 TallyEvent::Canceled(parsed) => {
887 assert_eq!(parsed.merchant, merchant);
888 assert_eq!(parsed.plan, plan);
889 assert_eq!(parsed.subscriber, subscriber);
890 }
891 _ => panic!("Expected Canceled event"),
892 }
893 }
894
895 #[test]
896 fn test_parse_payment_failed_event() {
897 let merchant = Pubkey::from(Keypair::new().pubkey().to_bytes());
898 let plan = Pubkey::from(Keypair::new().pubkey().to_bytes());
899 let subscriber = Pubkey::from(Keypair::new().pubkey().to_bytes());
900
901 let event = PaymentFailed {
902 merchant,
903 plan,
904 subscriber,
905 reason: "Insufficient funds".to_string(),
906 };
907
908 let encoded_data = create_test_event_data("PaymentFailed", &event);
909 let parsed_event = parse_single_event(&encoded_data).unwrap();
910
911 match parsed_event {
912 TallyEvent::PaymentFailed(parsed) => {
913 assert_eq!(parsed.merchant, merchant);
914 assert_eq!(parsed.plan, plan);
915 assert_eq!(parsed.subscriber, subscriber);
916 assert_eq!(parsed.reason, "Insufficient funds");
917 }
918 _ => panic!("Expected PaymentFailed event"),
919 }
920 }
921
922 #[test]
923 fn test_parse_single_event_invalid_base64() {
924 let result = parse_single_event("invalid_base64_!@#$%");
925 assert!(result.is_err());
926 if let Err(TallyError::ParseError(msg)) = result {
927 assert!(msg.contains("Failed to decode base64"));
928 }
929 }
930
931 #[test]
932 fn test_parse_single_event_too_short() {
933 let short_data = base64::prelude::BASE64_STANDARD.encode(vec![1, 2, 3, 4, 5, 6]);
935 let result = parse_single_event(&short_data);
936
937 assert!(result.is_err());
938 if let Err(TallyError::ParseError(msg)) = result {
939 assert!(msg.contains("Event data too short"));
940 }
941 }
942
943 #[test]
944 fn test_parse_single_event_unknown_discriminator() {
945 let mut data = vec![0xFF; 8]; data.extend_from_slice(&[1, 2, 3, 4]); let encoded_data = base64::prelude::BASE64_STANDARD.encode(data);
949
950 let result = parse_single_event(&encoded_data);
951 assert!(result.is_err());
952 if let Err(TallyError::ParseError(msg)) = result {
953 assert!(msg.contains("Unknown event discriminator"));
954 }
955 }
956
957 #[test]
958 fn test_parse_single_event_malformed_event_data() {
959 let discriminator = compute_event_discriminator("Subscribed");
961 let mut data = Vec::new();
962 data.extend_from_slice(&discriminator);
963 data.extend_from_slice(&[0xFF, 0xFF, 0xFF]); let encoded_data = base64::prelude::BASE64_STANDARD.encode(data);
965
966 let result = parse_single_event(&encoded_data);
967 assert!(result.is_err());
968 if let Err(TallyError::ParseError(msg)) = result {
969 assert!(msg.contains("Failed to deserialize Subscribed event"));
970 }
971 }
972
973 #[test]
974 fn test_parse_events_from_logs() {
975 let program_id = crate::program_id();
976 let merchant = Pubkey::from(Keypair::new().pubkey().to_bytes());
977 let plan = Pubkey::from(Keypair::new().pubkey().to_bytes());
978 let subscriber = Pubkey::from(Keypair::new().pubkey().to_bytes());
979
980 let subscribed_event = Subscribed {
981 merchant,
982 plan,
983 subscriber,
984 amount: 1_000_000,
985 };
986
987 let canceled_event = Canceled {
988 merchant,
989 plan,
990 subscriber,
991 };
992
993 let subscribed_data = create_test_event_data("Subscribed", &subscribed_event);
994 let canceled_data = create_test_event_data("Canceled", &canceled_event);
995
996 let logs = vec![
997 "Program 11111111111111111111111111111111 invoke [1]".to_string(),
998 format!("Program data: {} {}", program_id, subscribed_data),
999 "Program log: Some other log".to_string(),
1000 format!("Program data: {} {}", program_id, canceled_data),
1001 "Program 11111111111111111111111111111111 success".to_string(),
1002 ];
1003
1004 let events = parse_events_from_logs(&logs, &program_id).unwrap();
1005 assert_eq!(events.len(), 2);
1006
1007 match &events[0] {
1009 TallyEvent::Subscribed(parsed) => {
1010 assert_eq!(parsed.merchant, merchant);
1011 assert_eq!(parsed.plan, plan);
1012 assert_eq!(parsed.subscriber, subscriber);
1013 assert_eq!(parsed.amount, 1_000_000);
1014 }
1015 _ => panic!("Expected first event to be Subscribed"),
1016 }
1017
1018 match &events[1] {
1020 TallyEvent::Canceled(parsed) => {
1021 assert_eq!(parsed.merchant, merchant);
1022 assert_eq!(parsed.plan, plan);
1023 assert_eq!(parsed.subscriber, subscriber);
1024 }
1025 _ => panic!("Expected second event to be Canceled"),
1026 }
1027 }
1028
1029 #[test]
1030 fn test_parse_events_from_logs_with_malformed_data() {
1031 let program_id = crate::program_id();
1032 let merchant = Pubkey::from(Keypair::new().pubkey().to_bytes());
1033 let plan = Pubkey::from(Keypair::new().pubkey().to_bytes());
1034 let subscriber = Pubkey::from(Keypair::new().pubkey().to_bytes());
1035
1036 let valid_event = Subscribed {
1037 merchant,
1038 plan,
1039 subscriber,
1040 amount: 1_000_000,
1041 };
1042
1043 let valid_data = create_test_event_data("Subscribed", &valid_event);
1044
1045 let logs = vec![
1046 "Program 11111111111111111111111111111111 invoke [1]".to_string(),
1047 format!("Program data: {} {}", program_id, valid_data),
1048 format!("Program data: {} invalid_base64_!@#$%", program_id), "Program 11111111111111111111111111111111 success".to_string(),
1050 ];
1051
1052 let events = parse_events_from_logs(&logs, &program_id).unwrap();
1053 assert_eq!(events.len(), 1);
1055
1056 match &events[0] {
1057 TallyEvent::Subscribed(parsed) => {
1058 assert_eq!(parsed.merchant, merchant);
1059 assert_eq!(parsed.amount, 1_000_000);
1060 }
1061 _ => panic!("Expected event to be Subscribed"),
1062 }
1063 }
1064
1065 #[test]
1066 fn test_parse_events_from_logs_empty() {
1067 let program_id = crate::program_id();
1068 let logs = vec![
1069 "Program 11111111111111111111111111111111 invoke [1]".to_string(),
1070 "Program log: No events here".to_string(),
1071 "Program 11111111111111111111111111111111 success".to_string(),
1072 ];
1073
1074 let events = parse_events_from_logs(&logs, &program_id).unwrap();
1075 assert_eq!(events.len(), 0);
1076 }
1077
1078 #[test]
1079 fn test_parse_events_from_logs_different_program() {
1080 let program_id = crate::program_id();
1081 let other_program_id = Pubkey::from(Keypair::new().pubkey().to_bytes());
1082
1083 let merchant = Pubkey::from(Keypair::new().pubkey().to_bytes());
1084 let plan = Pubkey::from(Keypair::new().pubkey().to_bytes());
1085 let subscriber = Pubkey::from(Keypair::new().pubkey().to_bytes());
1086
1087 let event = Subscribed {
1088 merchant,
1089 plan,
1090 subscriber,
1091 amount: 1_000_000,
1092 };
1093
1094 let event_data = create_test_event_data("Subscribed", &event);
1095
1096 let logs = vec![
1097 "Program 11111111111111111111111111111111 invoke [1]".to_string(),
1098 format!("Program data: {} {}", other_program_id, event_data), "Program 11111111111111111111111111111111 success".to_string(),
1100 ];
1101
1102 let events = parse_events_from_logs(&logs, &program_id).unwrap();
1103 assert_eq!(events.len(), 0);
1105 }
1106}