use crate::{error::Result, TallyError};
use anchor_client::solana_sdk::{signature::Signature, transaction::TransactionError};
use anchor_lang::prelude::*;
use base64::prelude::*;
use chrono;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
)]
pub struct Subscribed {
pub merchant: Pubkey,
pub plan: Pubkey,
pub subscriber: Pubkey,
pub amount: u64,
}
#[derive(
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
)]
pub struct Renewed {
pub merchant: Pubkey,
pub plan: Pubkey,
pub subscriber: Pubkey,
pub amount: u64,
}
#[derive(
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
)]
pub struct Canceled {
pub merchant: Pubkey,
pub plan: Pubkey,
pub subscriber: Pubkey,
}
#[derive(
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
)]
pub struct PaymentFailed {
pub merchant: Pubkey,
pub plan: Pubkey,
pub subscriber: Pubkey,
pub reason: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum TallyEvent {
Subscribed(Subscribed),
Renewed(Renewed),
Canceled(Canceled),
PaymentFailed(PaymentFailed),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ParsedEventWithContext {
pub signature: Signature,
pub slot: u64,
pub block_time: Option<i64>,
pub success: bool,
pub event: TallyEvent,
pub log_index: usize,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct StreamableEventData {
pub event_type: String,
pub merchant_pda: String,
pub transaction_signature: String,
pub timestamp: i64,
pub metadata: HashMap<String, String>,
pub amount: Option<u64>,
pub plan_address: Option<String>,
pub subscription_address: Option<String>,
}
impl ParsedEventWithContext {
#[must_use]
pub const fn new(
signature: Signature,
slot: u64,
block_time: Option<i64>,
success: bool,
event: TallyEvent,
log_index: usize,
) -> Self {
Self {
signature,
slot,
block_time,
success,
event,
log_index,
}
}
#[must_use]
pub fn to_streamable(&self) -> StreamableEventData {
let (event_type, merchant_pda, plan_address, subscriber, amount, reason) = match &self.event
{
TallyEvent::Subscribed(e) => (
"subscribed".to_string(),
e.merchant.to_string(),
Some(e.plan.to_string()),
Some(e.subscriber.to_string()),
Some(e.amount),
None,
),
TallyEvent::Renewed(e) => (
"renewed".to_string(),
e.merchant.to_string(),
Some(e.plan.to_string()),
Some(e.subscriber.to_string()),
Some(e.amount),
None,
),
TallyEvent::Canceled(e) => (
"canceled".to_string(),
e.merchant.to_string(),
Some(e.plan.to_string()),
Some(e.subscriber.to_string()),
None,
None,
),
TallyEvent::PaymentFailed(e) => (
"payment_failed".to_string(),
e.merchant.to_string(),
Some(e.plan.to_string()),
Some(e.subscriber.to_string()),
None,
Some(e.reason.clone()),
),
};
let mut metadata = HashMap::new();
if let Some(subscriber) = subscriber {
metadata.insert("subscriber".to_string(), subscriber);
}
if let Some(reason) = reason {
metadata.insert("reason".to_string(), reason);
}
metadata.insert("slot".to_string(), self.slot.to_string());
metadata.insert("success".to_string(), self.success.to_string());
let subscription_address = if plan_address.is_some() && metadata.contains_key("subscriber")
{
let subscriber_str = metadata.get("subscriber").map_or("unknown", String::as_str);
Some(format!(
"subscription_{}_{}",
plan_address.as_deref().unwrap_or("unknown"),
subscriber_str
))
} else {
None
};
StreamableEventData {
event_type,
merchant_pda,
transaction_signature: self.signature.to_string(),
timestamp: self.block_time.unwrap_or(0),
metadata,
amount,
plan_address,
subscription_address,
}
}
#[must_use]
pub const fn is_successful(&self) -> bool {
self.success
}
#[must_use]
pub const fn get_merchant(&self) -> Option<Pubkey> {
match &self.event {
TallyEvent::Subscribed(e) => Some(e.merchant),
TallyEvent::Renewed(e) => Some(e.merchant),
TallyEvent::Canceled(e) => Some(e.merchant),
TallyEvent::PaymentFailed(e) => Some(e.merchant),
}
}
#[must_use]
pub const fn get_plan(&self) -> Option<Pubkey> {
match &self.event {
TallyEvent::Subscribed(e) => Some(e.plan),
TallyEvent::Renewed(e) => Some(e.plan),
TallyEvent::Canceled(e) => Some(e.plan),
TallyEvent::PaymentFailed(e) => Some(e.plan),
}
}
#[must_use]
pub const fn get_subscriber(&self) -> Option<Pubkey> {
match &self.event {
TallyEvent::Subscribed(e) => Some(e.subscriber),
TallyEvent::Renewed(e) => Some(e.subscriber),
TallyEvent::Canceled(e) => Some(e.subscriber),
TallyEvent::PaymentFailed(e) => Some(e.subscriber),
}
}
#[must_use]
pub const fn get_amount(&self) -> Option<u64> {
match &self.event {
TallyEvent::Subscribed(e) => Some(e.amount),
TallyEvent::Renewed(e) => Some(e.amount),
TallyEvent::Canceled(_) | TallyEvent::PaymentFailed(_) => None,
}
}
#[must_use]
pub fn get_event_type_string(&self) -> String {
match &self.event {
TallyEvent::Subscribed(_) => "Subscribed".to_string(),
TallyEvent::Renewed(_) => "Renewed".to_string(),
TallyEvent::Canceled(_) => "Canceled".to_string(),
TallyEvent::PaymentFailed(_) => "PaymentFailed".to_string(),
}
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn format_amount(&self) -> Option<f64> {
self.get_amount().map(|amount| amount as f64 / 1_000_000.0)
}
#[must_use]
pub fn format_timestamp(&self) -> String {
self.block_time.map_or_else(
|| "Pending".to_string(),
|timestamp| {
chrono::DateTime::from_timestamp(timestamp, 0)
.map_or_else(|| "Unknown".to_string(), |dt| dt.to_rfc3339())
},
)
}
#[must_use]
pub const fn affects_revenue(&self) -> bool {
matches!(
&self.event,
TallyEvent::Subscribed(_) | TallyEvent::Renewed(_)
)
}
#[must_use]
pub const fn affects_subscription_count(&self) -> bool {
matches!(
&self.event,
TallyEvent::Subscribed(_) | TallyEvent::Canceled(_)
)
}
#[must_use]
pub fn get_failure_reason(&self) -> Option<&str> {
match &self.event {
TallyEvent::PaymentFailed(e) => Some(&e.reason),
_ => None,
}
}
}
fn compute_event_discriminator(event_name: &str) -> [u8; 8] {
use anchor_lang::solana_program::hash;
let preimage = format!("event:{event_name}");
let hash_result = hash::hash(preimage.as_bytes());
let mut discriminator = [0u8; 8];
discriminator.copy_from_slice(&hash_result.to_bytes()[..8]);
discriminator
}
fn get_event_discriminators() -> HashMap<[u8; 8], &'static str> {
let mut discriminators = HashMap::new();
discriminators.insert(compute_event_discriminator("Subscribed"), "Subscribed");
discriminators.insert(compute_event_discriminator("Renewed"), "Renewed");
discriminators.insert(compute_event_discriminator("Canceled"), "Canceled");
discriminators.insert(
compute_event_discriminator("PaymentFailed"),
"PaymentFailed",
);
discriminators
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TallyReceipt {
pub signature: Signature,
pub block_time: Option<i64>,
pub slot: u64,
pub success: bool,
pub error: Option<String>,
pub events: Vec<TallyEvent>,
pub logs: Vec<String>,
pub compute_units_consumed: Option<u64>,
pub fee: u64,
}
pub fn parse_events_with_context(
logs: &[String],
program_id: &Pubkey,
signature: Signature,
slot: u64,
block_time: Option<i64>,
success: bool,
) -> Result<Vec<ParsedEventWithContext>> {
let events = parse_events_from_logs(logs, program_id)?;
let mut parsed_events = Vec::new();
for (log_index, event) in events.into_iter().enumerate() {
parsed_events.push(ParsedEventWithContext::new(
signature, slot, block_time, success, event, log_index,
));
}
Ok(parsed_events)
}
pub fn parse_events_from_logs(logs: &[String], program_id: &Pubkey) -> Result<Vec<TallyEvent>> {
let mut events = Vec::new();
let program_data_prefix = format!("Program data: {program_id} ");
for log in logs {
if let Some(data_start) = log.find(&program_data_prefix) {
let event_data = &log[data_start.saturating_add(program_data_prefix.len())..];
if let Ok(event) = parse_single_event(event_data) {
events.push(event);
}
}
}
Ok(events)
}
pub fn parse_single_event(data: &str) -> Result<TallyEvent> {
let decoded_data = base64::prelude::BASE64_STANDARD
.decode(data)
.map_err(|e| TallyError::ParseError(format!("Failed to decode base64: {e}")))?;
if decoded_data.len() < 8 {
return Err(TallyError::ParseError(
"Event data too short, must be at least 8 bytes for discriminator".to_string(),
));
}
let mut discriminator = [0u8; 8];
discriminator.copy_from_slice(&decoded_data[..8]);
let event_data = &decoded_data[8..];
let discriminators = get_event_discriminators();
let event_type = discriminators.get(&discriminator).ok_or_else(|| {
TallyError::ParseError(format!("Unknown event discriminator: {discriminator:?}"))
})?;
match *event_type {
"Subscribed" => {
let event = Subscribed::try_from_slice(event_data).map_err(|e| {
TallyError::ParseError(format!("Failed to deserialize Subscribed event: {e}"))
})?;
Ok(TallyEvent::Subscribed(event))
}
"Renewed" => {
let event = Renewed::try_from_slice(event_data).map_err(|e| {
TallyError::ParseError(format!("Failed to deserialize Renewed event: {e}"))
})?;
Ok(TallyEvent::Renewed(event))
}
"Canceled" => {
let event = Canceled::try_from_slice(event_data).map_err(|e| {
TallyError::ParseError(format!("Failed to deserialize Canceled event: {e}"))
})?;
Ok(TallyEvent::Canceled(event))
}
"PaymentFailed" => {
let event = PaymentFailed::try_from_slice(event_data).map_err(|e| {
TallyError::ParseError(format!("Failed to deserialize PaymentFailed event: {e}"))
})?;
Ok(TallyEvent::PaymentFailed(event))
}
_ => Err(TallyError::ParseError(format!(
"Unhandled event type: {event_type}"
))),
}
}
pub struct ReceiptParams {
pub signature: Signature,
pub block_time: Option<i64>,
pub slot: u64,
pub success: bool,
pub error: Option<TransactionError>,
pub logs: Vec<String>,
pub compute_units_consumed: Option<u64>,
pub fee: u64,
pub program_id: Pubkey,
}
pub fn create_receipt(params: ReceiptParams) -> Result<TallyReceipt> {
let events = parse_events_from_logs(¶ms.logs, ¶ms.program_id)?;
Ok(TallyReceipt {
signature: params.signature,
block_time: params.block_time,
slot: params.slot,
success: params.success,
error: params.error.map(|e| format!("{e:?}")),
events,
logs: params.logs,
compute_units_consumed: params.compute_units_consumed,
fee: params.fee,
})
}
#[allow(clippy::too_many_arguments)] pub fn create_receipt_legacy(
signature: Signature,
block_time: Option<i64>,
slot: u64,
success: bool,
error: Option<TransactionError>,
logs: Vec<String>,
compute_units_consumed: Option<u64>,
fee: u64,
program_id: &Pubkey,
) -> Result<TallyReceipt> {
create_receipt(ReceiptParams {
signature,
block_time,
slot,
success,
error,
logs,
compute_units_consumed,
fee,
program_id: *program_id,
})
}
#[must_use]
pub fn extract_memo_from_logs(logs: &[String]) -> Option<String> {
for log in logs {
if log.starts_with("Program log: Memo (len ") {
if let Some(start) = log.find("): \"") {
let memo_start = start.saturating_add(4); if let Some(end) = log.rfind('"') {
if end > memo_start {
return Some(log[memo_start..end].to_string());
}
}
}
} else if log.starts_with("Program log: ") && log.contains("memo:") {
if let Some(memo_start) = log.find("memo:") {
let memo_content = &log[memo_start.saturating_add(5)..].trim();
return Some((*memo_content).to_string());
}
}
}
None
}
impl TallyReceipt {
#[must_use]
pub fn get_subscribed_event(&self) -> Option<&Subscribed> {
self.events.iter().find_map(|event| match event {
TallyEvent::Subscribed(e) => Some(e),
_ => None,
})
}
#[must_use]
pub fn get_renewed_event(&self) -> Option<&Renewed> {
self.events.iter().find_map(|event| match event {
TallyEvent::Renewed(e) => Some(e),
_ => None,
})
}
#[must_use]
pub fn get_canceled_event(&self) -> Option<&Canceled> {
self.events.iter().find_map(|event| match event {
TallyEvent::Canceled(e) => Some(e),
_ => None,
})
}
#[must_use]
pub fn get_payment_failed_event(&self) -> Option<&PaymentFailed> {
self.events.iter().find_map(|event| match event {
TallyEvent::PaymentFailed(e) => Some(e),
_ => None,
})
}
#[must_use]
pub fn extract_memo(&self) -> Option<String> {
extract_memo_from_logs(&self.logs)
}
#[must_use]
pub fn is_subscription_success(&self) -> bool {
self.success
&& (self.get_subscribed_event().is_some()
|| self.get_renewed_event().is_some()
|| self.get_canceled_event().is_some())
}
}
#[cfg(test)]
mod tests {
use super::*;
use anchor_client::solana_sdk::signature::{Keypair, Signer};
#[test]
fn test_extract_memo_from_logs() {
let logs = vec![
"Program 11111111111111111111111111111111 invoke [1]".to_string(),
"Program log: Memo (len 12): \"Test message\"".to_string(),
"Program 11111111111111111111111111111111 consumed 1000 of 200000 compute units"
.to_string(),
];
let memo = extract_memo_from_logs(&logs);
assert_eq!(memo, Some("Test message".to_string()));
}
#[test]
fn test_extract_memo_alternative_format() {
let logs = vec!["Program log: Processing memo: Hello world".to_string()];
let memo = extract_memo_from_logs(&logs);
assert_eq!(memo, Some("Hello world".to_string()));
}
#[test]
fn test_extract_memo_none() {
let logs = vec![
"Program 11111111111111111111111111111111 invoke [1]".to_string(),
"Program 11111111111111111111111111111111 consumed 1000 of 200000 compute units"
.to_string(),
];
let memo = extract_memo_from_logs(&logs);
assert_eq!(memo, None);
}
#[test]
fn test_tally_receipt_event_getters() {
let signature = Signature::default();
let merchant = Pubkey::from(Keypair::new().pubkey().to_bytes());
let plan = Pubkey::from(Keypair::new().pubkey().to_bytes());
let subscriber = Pubkey::from(Keypair::new().pubkey().to_bytes());
let subscribed_event = Subscribed {
merchant,
plan,
subscriber,
amount: 1_000_000, };
let receipt = TallyReceipt {
signature,
block_time: Some(1_640_995_200), slot: 100,
success: true,
error: None,
events: vec![TallyEvent::Subscribed(subscribed_event.clone())],
logs: vec![],
compute_units_consumed: Some(5000),
fee: 5000,
};
assert_eq!(receipt.get_subscribed_event(), Some(&subscribed_event));
assert_eq!(receipt.get_renewed_event(), None);
assert_eq!(receipt.get_canceled_event(), None);
assert_eq!(receipt.get_payment_failed_event(), None);
assert!(receipt.is_subscription_success());
}
#[test]
fn test_tally_receipt_failed_transaction() {
let signature = Signature::default();
let receipt = TallyReceipt {
signature,
block_time: Some(1_640_995_200),
slot: 100,
success: false,
error: Some("InsufficientFunds".to_string()),
events: vec![],
logs: vec![],
compute_units_consumed: Some(1000),
fee: 5000,
};
assert!(!receipt.is_subscription_success());
}
#[test]
fn test_create_receipt() {
let signature = Signature::default();
let program_id = crate::program_id();
let receipt = create_receipt(ReceiptParams {
signature,
block_time: Some(1_640_995_200),
slot: 100,
success: true,
error: None,
logs: vec!["Program invoked".to_string()],
compute_units_consumed: Some(5000),
fee: 5000,
program_id,
})
.unwrap();
assert_eq!(receipt.signature, signature);
assert_eq!(receipt.slot, 100);
assert!(receipt.success);
assert_eq!(receipt.error, None);
assert_eq!(receipt.fee, 5000);
}
fn create_test_event_data(event_name: &str, event_struct: &impl AnchorSerialize) -> String {
let discriminator = compute_event_discriminator(event_name);
let mut event_data = Vec::new();
event_data.extend_from_slice(&discriminator);
event_struct.serialize(&mut event_data).unwrap();
base64::prelude::BASE64_STANDARD.encode(event_data)
}
#[test]
fn test_compute_event_discriminator() {
let subscribed_disc = compute_event_discriminator("Subscribed");
let renewed_disc = compute_event_discriminator("Renewed");
let canceled_disc = compute_event_discriminator("Canceled");
let payment_failed_disc = compute_event_discriminator("PaymentFailed");
assert_ne!(subscribed_disc, renewed_disc);
assert_ne!(subscribed_disc, canceled_disc);
assert_ne!(subscribed_disc, payment_failed_disc);
assert_ne!(renewed_disc, canceled_disc);
assert_ne!(renewed_disc, payment_failed_disc);
assert_ne!(canceled_disc, payment_failed_disc);
assert_eq!(subscribed_disc, compute_event_discriminator("Subscribed"));
assert_eq!(renewed_disc, compute_event_discriminator("Renewed"));
}
#[test]
fn test_get_event_discriminators() {
let discriminators = get_event_discriminators();
assert_eq!(discriminators.len(), 4);
assert!(discriminators.contains_key(&compute_event_discriminator("Subscribed")));
assert!(discriminators.contains_key(&compute_event_discriminator("Renewed")));
assert!(discriminators.contains_key(&compute_event_discriminator("Canceled")));
assert!(discriminators.contains_key(&compute_event_discriminator("PaymentFailed")));
}
#[test]
fn test_parse_subscribed_event() {
let merchant = Pubkey::from(Keypair::new().pubkey().to_bytes());
let plan = Pubkey::from(Keypair::new().pubkey().to_bytes());
let subscriber = Pubkey::from(Keypair::new().pubkey().to_bytes());
let event = Subscribed {
merchant,
plan,
subscriber,
amount: 5_000_000, };
let encoded_data = create_test_event_data("Subscribed", &event);
let parsed_event = parse_single_event(&encoded_data).unwrap();
match parsed_event {
TallyEvent::Subscribed(parsed) => {
assert_eq!(parsed.merchant, merchant);
assert_eq!(parsed.plan, plan);
assert_eq!(parsed.subscriber, subscriber);
assert_eq!(parsed.amount, 5_000_000);
}
_ => panic!("Expected Subscribed event"),
}
}
#[test]
fn test_parse_renewed_event() {
let merchant = Pubkey::from(Keypair::new().pubkey().to_bytes());
let plan = Pubkey::from(Keypair::new().pubkey().to_bytes());
let subscriber = Pubkey::from(Keypair::new().pubkey().to_bytes());
let event = Renewed {
merchant,
plan,
subscriber,
amount: 10_000_000, };
let encoded_data = create_test_event_data("Renewed", &event);
let parsed_event = parse_single_event(&encoded_data).unwrap();
match parsed_event {
TallyEvent::Renewed(parsed) => {
assert_eq!(parsed.merchant, merchant);
assert_eq!(parsed.plan, plan);
assert_eq!(parsed.subscriber, subscriber);
assert_eq!(parsed.amount, 10_000_000);
}
_ => panic!("Expected Renewed event"),
}
}
#[test]
fn test_parse_canceled_event() {
let merchant = Pubkey::from(Keypair::new().pubkey().to_bytes());
let plan = Pubkey::from(Keypair::new().pubkey().to_bytes());
let subscriber = Pubkey::from(Keypair::new().pubkey().to_bytes());
let event = Canceled {
merchant,
plan,
subscriber,
};
let encoded_data = create_test_event_data("Canceled", &event);
let parsed_event = parse_single_event(&encoded_data).unwrap();
match parsed_event {
TallyEvent::Canceled(parsed) => {
assert_eq!(parsed.merchant, merchant);
assert_eq!(parsed.plan, plan);
assert_eq!(parsed.subscriber, subscriber);
}
_ => panic!("Expected Canceled event"),
}
}
#[test]
fn test_parse_payment_failed_event() {
let merchant = Pubkey::from(Keypair::new().pubkey().to_bytes());
let plan = Pubkey::from(Keypair::new().pubkey().to_bytes());
let subscriber = Pubkey::from(Keypair::new().pubkey().to_bytes());
let event = PaymentFailed {
merchant,
plan,
subscriber,
reason: "Insufficient funds".to_string(),
};
let encoded_data = create_test_event_data("PaymentFailed", &event);
let parsed_event = parse_single_event(&encoded_data).unwrap();
match parsed_event {
TallyEvent::PaymentFailed(parsed) => {
assert_eq!(parsed.merchant, merchant);
assert_eq!(parsed.plan, plan);
assert_eq!(parsed.subscriber, subscriber);
assert_eq!(parsed.reason, "Insufficient funds");
}
_ => panic!("Expected PaymentFailed event"),
}
}
#[test]
fn test_parse_single_event_invalid_base64() {
let result = parse_single_event("invalid_base64_!@#$%");
assert!(result.is_err());
if let Err(TallyError::ParseError(msg)) = result {
assert!(msg.contains("Failed to decode base64"));
}
}
#[test]
fn test_parse_single_event_too_short() {
let short_data = base64::prelude::BASE64_STANDARD.encode(vec![1, 2, 3, 4, 5, 6]);
let result = parse_single_event(&short_data);
assert!(result.is_err());
if let Err(TallyError::ParseError(msg)) = result {
assert!(msg.contains("Event data too short"));
}
}
#[test]
fn test_parse_single_event_unknown_discriminator() {
let mut data = vec![0xFF; 8]; data.extend_from_slice(&[1, 2, 3, 4]); let encoded_data = base64::prelude::BASE64_STANDARD.encode(data);
let result = parse_single_event(&encoded_data);
assert!(result.is_err());
if let Err(TallyError::ParseError(msg)) = result {
assert!(msg.contains("Unknown event discriminator"));
}
}
#[test]
fn test_parse_single_event_malformed_event_data() {
let discriminator = compute_event_discriminator("Subscribed");
let mut data = Vec::new();
data.extend_from_slice(&discriminator);
data.extend_from_slice(&[0xFF, 0xFF, 0xFF]); let encoded_data = base64::prelude::BASE64_STANDARD.encode(data);
let result = parse_single_event(&encoded_data);
assert!(result.is_err());
if let Err(TallyError::ParseError(msg)) = result {
assert!(msg.contains("Failed to deserialize Subscribed event"));
}
}
#[test]
fn test_parse_events_from_logs() {
let program_id = crate::program_id();
let merchant = Pubkey::from(Keypair::new().pubkey().to_bytes());
let plan = Pubkey::from(Keypair::new().pubkey().to_bytes());
let subscriber = Pubkey::from(Keypair::new().pubkey().to_bytes());
let subscribed_event = Subscribed {
merchant,
plan,
subscriber,
amount: 1_000_000,
};
let canceled_event = Canceled {
merchant,
plan,
subscriber,
};
let subscribed_data = create_test_event_data("Subscribed", &subscribed_event);
let canceled_data = create_test_event_data("Canceled", &canceled_event);
let logs = vec![
"Program 11111111111111111111111111111111 invoke [1]".to_string(),
format!("Program data: {} {}", program_id, subscribed_data),
"Program log: Some other log".to_string(),
format!("Program data: {} {}", program_id, canceled_data),
"Program 11111111111111111111111111111111 success".to_string(),
];
let events = parse_events_from_logs(&logs, &program_id).unwrap();
assert_eq!(events.len(), 2);
match &events[0] {
TallyEvent::Subscribed(parsed) => {
assert_eq!(parsed.merchant, merchant);
assert_eq!(parsed.plan, plan);
assert_eq!(parsed.subscriber, subscriber);
assert_eq!(parsed.amount, 1_000_000);
}
_ => panic!("Expected first event to be Subscribed"),
}
match &events[1] {
TallyEvent::Canceled(parsed) => {
assert_eq!(parsed.merchant, merchant);
assert_eq!(parsed.plan, plan);
assert_eq!(parsed.subscriber, subscriber);
}
_ => panic!("Expected second event to be Canceled"),
}
}
#[test]
fn test_parse_events_from_logs_with_malformed_data() {
let program_id = crate::program_id();
let merchant = Pubkey::from(Keypair::new().pubkey().to_bytes());
let plan = Pubkey::from(Keypair::new().pubkey().to_bytes());
let subscriber = Pubkey::from(Keypair::new().pubkey().to_bytes());
let valid_event = Subscribed {
merchant,
plan,
subscriber,
amount: 1_000_000,
};
let valid_data = create_test_event_data("Subscribed", &valid_event);
let logs = vec![
"Program 11111111111111111111111111111111 invoke [1]".to_string(),
format!("Program data: {} {}", program_id, valid_data),
format!("Program data: {} invalid_base64_!@#$%", program_id), "Program 11111111111111111111111111111111 success".to_string(),
];
let events = parse_events_from_logs(&logs, &program_id).unwrap();
assert_eq!(events.len(), 1);
match &events[0] {
TallyEvent::Subscribed(parsed) => {
assert_eq!(parsed.merchant, merchant);
assert_eq!(parsed.amount, 1_000_000);
}
_ => panic!("Expected event to be Subscribed"),
}
}
#[test]
fn test_parse_events_from_logs_empty() {
let program_id = crate::program_id();
let logs = vec![
"Program 11111111111111111111111111111111 invoke [1]".to_string(),
"Program log: No events here".to_string(),
"Program 11111111111111111111111111111111 success".to_string(),
];
let events = parse_events_from_logs(&logs, &program_id).unwrap();
assert_eq!(events.len(), 0);
}
#[test]
fn test_parse_events_from_logs_different_program() {
let program_id = crate::program_id();
let other_program_id = Pubkey::from(Keypair::new().pubkey().to_bytes());
let merchant = Pubkey::from(Keypair::new().pubkey().to_bytes());
let plan = Pubkey::from(Keypair::new().pubkey().to_bytes());
let subscriber = Pubkey::from(Keypair::new().pubkey().to_bytes());
let event = Subscribed {
merchant,
plan,
subscriber,
amount: 1_000_000,
};
let event_data = create_test_event_data("Subscribed", &event);
let logs = vec![
"Program 11111111111111111111111111111111 invoke [1]".to_string(),
format!("Program data: {} {}", other_program_id, event_data), "Program 11111111111111111111111111111111 success".to_string(),
];
let events = parse_events_from_logs(&logs, &program_id).unwrap();
assert_eq!(events.len(), 0);
}
}