tally_sdk/
events.rs

1//! Event parsing utilities for Tally program events and structured receipts
2
3use 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/// Event emitted when a subscription is successfully started
12#[derive(
13    Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
14)]
15pub struct Subscribed {
16    /// The merchant who owns the subscription plan
17    pub merchant: Pubkey,
18    /// The subscription plan being subscribed to
19    pub plan: Pubkey,
20    /// The subscriber's public key
21    pub subscriber: Pubkey,
22    /// The amount paid for the subscription (in USDC micro-units)
23    pub amount: u64,
24}
25
26/// Event emitted when a subscription is successfully renewed
27#[derive(
28    Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
29)]
30pub struct Renewed {
31    /// The merchant who owns the subscription plan
32    pub merchant: Pubkey,
33    /// The subscription plan being renewed
34    pub plan: Pubkey,
35    /// The subscriber's public key
36    pub subscriber: Pubkey,
37    /// The amount paid for the renewal (in USDC micro-units)
38    pub amount: u64,
39}
40
41/// Event emitted when a subscription is canceled
42#[derive(
43    Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
44)]
45pub struct Canceled {
46    /// The merchant who owns the subscription plan
47    pub merchant: Pubkey,
48    /// The subscription plan being canceled
49    pub plan: Pubkey,
50    /// The subscriber's public key
51    pub subscriber: Pubkey,
52}
53
54/// Event emitted when a subscription payment fails
55#[derive(
56    Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
57)]
58pub struct PaymentFailed {
59    /// The merchant who owns the subscription plan
60    pub merchant: Pubkey,
61    /// The subscription plan where payment failed
62    pub plan: Pubkey,
63    /// The subscriber's public key
64    pub subscriber: Pubkey,
65    /// The reason for payment failure (encoded as string for off-chain analysis)
66    pub reason: String,
67}
68
69/// All possible Tally program events
70#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
71pub enum TallyEvent {
72    /// Subscription started
73    Subscribed(Subscribed),
74    /// Subscription renewed
75    Renewed(Renewed),
76    /// Subscription canceled
77    Canceled(Canceled),
78    /// Payment failed
79    PaymentFailed(PaymentFailed),
80}
81
82/// Enhanced parsed event with transaction context for RPC queries and WebSocket streaming
83#[derive(Clone, Debug, Serialize, Deserialize)]
84pub struct ParsedEventWithContext {
85    /// Transaction signature that contains this event
86    pub signature: Signature,
87    /// Slot number where transaction was processed
88    pub slot: u64,
89    /// Block time (Unix timestamp)
90    pub block_time: Option<i64>,
91    /// Transaction success status
92    pub success: bool,
93    /// The parsed Tally event
94    pub event: TallyEvent,
95    /// Log index within the transaction
96    pub log_index: usize,
97}
98
99/// WebSocket-friendly event data for dashboard streaming
100#[derive(Clone, Debug, Serialize, Deserialize)]
101pub struct StreamableEventData {
102    /// Event type as string
103    pub event_type: String,
104    /// Merchant PDA
105    pub merchant_pda: String,
106    /// Transaction signature
107    pub transaction_signature: String,
108    /// Event timestamp
109    pub timestamp: i64,
110    /// Event metadata
111    pub metadata: HashMap<String, String>,
112    /// Amount involved (if applicable)
113    pub amount: Option<u64>,
114    /// Plan address (if applicable)
115    pub plan_address: Option<String>,
116    /// Subscription address (if applicable)
117    pub subscription_address: Option<String>,
118}
119
120impl ParsedEventWithContext {
121    /// Create a new `ParsedEventWithContext` from components
122    #[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    /// Convert to streamable event data for WebSocket
142    #[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        // Generate subscription address for events that have plan + subscriber
191        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    /// Check if this event was successful
216    #[must_use]
217    pub const fn is_successful(&self) -> bool {
218        self.success
219    }
220
221    /// Get the merchant pubkey from the event
222    #[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    /// Get the plan pubkey from the event
233    #[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    /// Get the subscriber pubkey from the event
244    #[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    /// Get the amount from the event (if applicable)
255    #[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    /// Get event type as string for display
265    #[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    /// Format amount as USDC (6 decimal places)
276    #[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    /// Get timestamp as formatted string
283    #[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    /// Check if this event affects revenue
295    #[must_use]
296    pub const fn affects_revenue(&self) -> bool {
297        matches!(
298            &self.event,
299            TallyEvent::Subscribed(_) | TallyEvent::Renewed(_)
300        )
301    }
302
303    /// Check if this event affects subscription count
304    #[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    /// Get the payment failure reason (if applicable)
313    #[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
322/// Compute the 8-byte discriminator for an Anchor event
323/// Formula: first 8 bytes of SHA256("event:<EventName>")
324fn 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
333/// Get all event discriminators for fast lookup
334fn 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/// Structured receipt for a Tally transaction
347#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
348pub struct TallyReceipt {
349    /// Transaction signature
350    pub signature: Signature,
351    /// Block time (Unix timestamp)
352    pub block_time: Option<i64>,
353    /// Transaction slot
354    pub slot: u64,
355    /// Whether the transaction was successful
356    pub success: bool,
357    /// Transaction error if any
358    pub error: Option<String>,
359    /// Parsed Tally events from this transaction
360    pub events: Vec<TallyEvent>,
361    /// Program logs from the transaction
362    pub logs: Vec<String>,
363    /// Compute units consumed
364    pub compute_units_consumed: Option<u64>,
365    /// Transaction fee in lamports
366    pub fee: u64,
367}
368
369/// Parse Tally events from transaction logs with transaction context
370///
371/// # Arguments
372/// * `logs` - The transaction logs to parse
373/// * `program_id` - The Tally program ID to filter events
374/// * `signature` - Transaction signature
375/// * `slot` - Transaction slot
376/// * `block_time` - Block time
377/// * `success` - Transaction success status
378///
379/// # Returns
380/// * `Ok(Vec<ParsedEventWithContext>)` - Parsed events with context
381/// * `Err(TallyError)` - If parsing fails
382pub 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
402/// Parse Tally events from transaction logs
403///
404/// # Arguments
405/// * `logs` - The transaction logs to parse
406/// * `program_id` - The Tally program ID to filter events
407///
408/// # Returns
409/// * `Ok(Vec<TallyEvent>)` - Parsed events
410/// * `Err(TallyError)` - If parsing fails
411pub 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
427/// Parse a single event from base64-encoded data
428///
429/// Anchor events are encoded as: discriminator (8 bytes) + borsh-serialized event data
430/// The discriminator is computed as the first 8 bytes of SHA256("event:<EventName>")
431pub fn parse_single_event(data: &str) -> Result<TallyEvent> {
432    // Decode base64 data
433    let decoded_data = base64::prelude::BASE64_STANDARD
434        .decode(data)
435        .map_err(|e| TallyError::ParseError(format!("Failed to decode base64: {e}")))?;
436
437    // Check minimum length for discriminator (8 bytes)
438    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    // Extract discriminator (first 8 bytes)
445    let mut discriminator = [0u8; 8];
446    discriminator.copy_from_slice(&decoded_data[..8]);
447
448    // Get event data (remaining bytes after discriminator)
449    let event_data = &decoded_data[8..];
450
451    // Determine event type based on discriminator
452    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    // Deserialize the event data using Borsh
458    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
489/// Parameters for creating a structured receipt
490pub struct ReceiptParams {
491    /// Transaction signature
492    pub signature: Signature,
493    /// Block time (Unix timestamp)
494    pub block_time: Option<i64>,
495    /// Transaction slot
496    pub slot: u64,
497    /// Whether transaction was successful
498    pub success: bool,
499    /// Transaction error if any
500    pub error: Option<TransactionError>,
501    /// Transaction logs
502    pub logs: Vec<String>,
503    /// Compute units consumed
504    pub compute_units_consumed: Option<u64>,
505    /// Transaction fee
506    pub fee: u64,
507    /// Program ID to parse events for
508    pub program_id: Pubkey,
509}
510
511/// Create a structured receipt from transaction components
512///
513/// # Arguments
514/// * `params` - Receipt creation parameters
515///
516/// # Returns
517/// * `Ok(TallyReceipt)` - Structured receipt
518/// * `Err(TallyError)` - If parsing fails
519pub fn create_receipt(params: ReceiptParams) -> Result<TallyReceipt> {
520    let events = parse_events_from_logs(&params.logs, &params.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/// Create a structured receipt from transaction components (legacy compatibility)
536///
537/// # Arguments
538/// * `signature` - Transaction signature
539/// * `block_time` - Block time (Unix timestamp)
540/// * `slot` - Transaction slot
541/// * `success` - Whether transaction was successful
542/// * `error` - Transaction error if any
543/// * `logs` - Transaction logs
544/// * `compute_units_consumed` - Compute units consumed
545/// * `fee` - Transaction fee
546/// * `program_id` - Program ID to parse events for
547///
548/// # Returns
549/// * `Ok(TallyReceipt)` - Structured receipt
550/// * `Err(TallyError)` - If parsing fails
551#[allow(clippy::too_many_arguments)] // Legacy function, will be deprecated
552pub 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/// Extract memo from transaction logs
577///
578/// # Arguments
579/// * `logs` - Transaction logs to search
580///
581/// # Returns
582/// * `Option<String>` - Found memo, if any
583#[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            // Format: "Program log: Memo (len N): \"message\""
588            if let Some(start) = log.find("): \"") {
589                let memo_start = start.saturating_add(4); // Skip "): \""
590                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            // Alternative memo format
598            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
607/// Find the first Tally event of a specific type in a receipt
608impl TallyReceipt {
609    /// Get the first Subscribed event, if any
610    #[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    /// Get the first Renewed event, if any
619    #[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    /// Get the first Canceled event, if any
628    #[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    /// Get the first `PaymentFailed` event, if any
637    #[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    /// Extract memo from transaction logs
646    #[must_use]
647    pub fn extract_memo(&self) -> Option<String> {
648        extract_memo_from_logs(&self.logs)
649    }
650
651    /// Check if this receipt represents a successful subscription operation
652    #[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, // 1 USDC
711        };
712
713        let receipt = TallyReceipt {
714            signature,
715            block_time: Some(1_640_995_200), // 2022-01-01
716            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    // Helper function to create base64-encoded event data for testing
777    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        // All discriminators should be unique
793        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        // Discriminators should be deterministic
801        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, // 5 USDC
827        };
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, // 10 USDC
854        };
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        // Create data with only 6 bytes (less than 8-byte discriminator requirement)
934        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        // Create data with unknown discriminator
946        let mut data = vec![0xFF; 8]; // Unknown discriminator
947        data.extend_from_slice(&[1, 2, 3, 4]); // Some event data
948        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        // Create data with correct discriminator but malformed event data
960        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]); // Malformed data that can't be deserialized as Subscribed
964        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        // Check first event (Subscribed)
1008        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        // Check second event (Canceled)
1019        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), // Malformed data - should be skipped
1049            "Program 11111111111111111111111111111111 success".to_string(),
1050        ];
1051
1052        let events = parse_events_from_logs(&logs, &program_id).unwrap();
1053        // Only the valid event should be parsed, malformed one should be skipped
1054        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), // Different program
1099            "Program 11111111111111111111111111111111 success".to_string(),
1100        ];
1101
1102        let events = parse_events_from_logs(&logs, &program_id).unwrap();
1103        // Should not parse events from different program
1104        assert_eq!(events.len(), 0);
1105    }
1106}