use std::ops::{Range, RangeInclusive};
use rialo_limits::MAX_STATIC_ACCOUNTS_PER_PACKET;
use rialo_s_instruction::{AccountMeta, Instruction};
use rialo_s_program::system_program;
use rialo_s_pubkey::Pubkey;
use rialo_types::Nonce;
use crate::error::SubscriptionError;
pub const RIALO_SUBSCRIBE_SEED: &str = "rialo_subscribe";
pub const RIALO_UNSUBSCRIBE_SEED: &str = "rialo_unsubscribe";
pub type TimestampRange = (u64, u64);
pub type Commmit = u64;
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
pub enum SubscriptionKind {
Persistent,
OneShot,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
pub enum SubscriberInstruction {
Subscribe {
nonce: Nonce,
handler: Subscription,
},
Unsubscribe {
nonce: Nonce,
},
Update {
nonce: Nonce,
handler: Subscription,
},
Destroy {
nonce: Nonce,
},
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
pub struct Predicate {
topic: String,
event_account: Option<Pubkey>,
timestamp_range: Option<TimestampRange>,
}
impl Predicate {
pub fn new(topic: impl Into<String>) -> Self {
Self {
topic: topic.into(),
event_account: None,
timestamp_range: None,
}
}
pub fn new_with_event_account(topic: impl Into<String>, event_account: Pubkey) -> Self {
Self {
topic: topic.into(),
event_account: Some(event_account),
timestamp_range: None,
}
}
pub fn new_with_optional_event_account(
topic: impl Into<String>,
event_account: Option<Pubkey>,
) -> Self {
Self {
topic: topic.into(),
event_account,
timestamp_range: None,
}
}
pub fn new_with_timestamp_range(range: Range<u64>) -> Self {
Self {
topic: rialo_events_core::types::CLOCK_TOPIC.to_string(),
event_account: Some(rialo_s_program::sysvar::clock::id()),
timestamp_range: Some((range.start, range.end)),
}
}
pub fn topic(&self) -> &str {
&self.topic
}
pub fn event_account(&self) -> Option<Pubkey> {
self.event_account
}
pub fn timestamp_range(&self) -> Option<TimestampRange> {
self.timestamp_range
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
pub struct Subscription {
pub subscriber: Pubkey,
pub predicate: Predicate,
pub instructions: Vec<Instruction>,
pub kind: SubscriptionKind,
pub active_commits: RangeInclusive<Commmit>,
}
impl Subscription {
pub fn new(
subscriber: Pubkey,
predicate: Predicate,
instructions: Vec<Instruction>,
kind: SubscriptionKind,
active_commits: RangeInclusive<u64>,
) -> Self {
Self {
subscriber,
predicate,
instructions,
kind,
active_commits,
}
}
pub fn sanitize_instructions(&self) -> Result<(), SubscriptionError> {
if self.instructions.is_empty() {
return Err(SubscriptionError::EmptyInstructions);
}
if let Some(invalid_signer) = self
.instructions
.iter()
.flat_map(|itx| itx.accounts.iter())
.find(|account| account.is_signer && account.pubkey != self.subscriber)
{
return Err(SubscriptionError::InvalidSigner(invalid_signer.pubkey));
}
let all_pubkeys: std::collections::HashSet<_> = self
.instructions
.iter()
.flat_map(|itx| {
std::iter::once(itx.program_id).chain(itx.accounts.iter().map(|acc| acc.pubkey))
})
.collect();
let total_accounts = all_pubkeys.len();
if total_accounts > MAX_STATIC_ACCOUNTS_PER_PACKET as usize {
return Err(SubscriptionError::TooManyAccounts {
actual: total_accounts,
max_accounts: MAX_STATIC_ACCOUNTS_PER_PACKET as usize,
});
}
Ok(())
}
}
impl SubscriberInstruction {
pub fn subscribe_to(signer: Pubkey, nonce: Nonce, mut handler: Subscription) -> Instruction {
if handler.subscriber != signer {
panic!("Handler subscriber must be the same as the signer");
}
if handler.instructions.is_empty() {
panic!("Handler instructions must not be empty");
}
let subscription_data_account = derive_subscription_address(signer, nonce);
if let SubscriptionKind::OneShot = handler.kind {
handler.instructions.push(Instruction::new_with_bincode(
crate::ID,
&SubscriberInstruction::Destroy { nonce },
vec![
AccountMeta::new(signer, true),
AccountMeta::new(subscription_data_account, false),
],
));
}
Instruction::new_with_bincode(
crate::ID,
&SubscriberInstruction::Subscribe { nonce, handler },
vec![
AccountMeta::new(signer, true),
AccountMeta::new(subscription_data_account, false),
AccountMeta::new_readonly(system_program::id(), false),
],
)
}
pub fn unsubscribe_from(signer: Pubkey, nonce: Nonce) -> Instruction {
let subscription_data_key = derive_subscription_address(signer, nonce);
Instruction::new_with_bincode(
crate::ID,
&SubscriberInstruction::Unsubscribe { nonce },
vec![
AccountMeta::new(signer, true),
AccountMeta::new(subscription_data_key, false),
],
)
}
pub fn update(signer: Pubkey, nonce: Nonce, mut handler: Subscription) -> Instruction {
let subscription_data_key = derive_subscription_address(signer, nonce);
if let SubscriptionKind::OneShot = handler.kind {
handler.instructions.push(Instruction::new_with_bincode(
crate::ID,
&SubscriberInstruction::Destroy { nonce },
vec![
AccountMeta::new_readonly(signer, true),
AccountMeta::new(subscription_data_key, false),
],
));
}
Instruction::new_with_bincode(
crate::ID,
&SubscriberInstruction::Update { nonce, handler },
vec![
AccountMeta::new(signer, true),
AccountMeta::new(subscription_data_key, false),
AccountMeta::new_readonly(system_program::id(), false),
],
)
}
}
pub fn derive_subscription_address<NONCE: Into<Nonce>>(subscriber: Pubkey, nonce: NONCE) -> Pubkey {
let nonce = nonce.into();
Pubkey::find_program_address(
&[
RIALO_SUBSCRIBE_SEED.as_bytes(),
subscriber.as_array(),
nonce.as_bytes(),
],
&crate::ID,
)
.0
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_subscribe_to_instruction() {
let signer = Pubkey::new_unique();
let handler = Subscription::new(
signer,
Predicate {
topic: String::from("MySubscription"),
event_account: Some(Pubkey::new_unique()),
timestamp_range: Some((1337, 1338)),
},
vec![Instruction::new_with_bincode(
Pubkey::new_unique(),
&["MyData"],
vec![],
)],
SubscriptionKind::Persistent,
0..=u64::MAX,
);
let instruction =
SubscriberInstruction::subscribe_to(signer, Nonce::default(), handler.clone());
assert_eq!(
instruction.data,
bincode::serialize(&SubscriberInstruction::Subscribe {
nonce: Nonce::default(),
handler,
})
.unwrap()
);
assert_eq!(instruction.program_id, crate::ID);
let subscription_data_account = derive_subscription_address(signer, Nonce::default());
assert_eq!(
instruction.accounts,
vec![
AccountMeta::new(signer, true),
AccountMeta::new(subscription_data_account, false),
AccountMeta::new_readonly(system_program::id(), false),
]
)
}
#[test]
fn test_delete_subscription_instruction() {
let signer = Pubkey::new_unique();
let instruction = SubscriberInstruction::unsubscribe_from(signer, Nonce::default());
assert_eq!(
instruction.data,
bincode::serialize(&SubscriberInstruction::Unsubscribe {
nonce: Nonce::default()
})
.unwrap()
);
assert_eq!(instruction.program_id, crate::ID);
let subscription_data_account = derive_subscription_address(signer, Nonce::default());
assert_eq!(
instruction.accounts,
vec![
AccountMeta::new(signer, true),
AccountMeta::new(subscription_data_account, false),
]
)
}
#[test]
fn test_update_subscription_instruction() {
let signer = Pubkey::new_unique();
let handler = Subscription {
subscriber: signer,
predicate: Predicate {
topic: String::from("MySubscription"),
event_account: Some(Pubkey::new_unique()),
timestamp_range: Some((1337, 1338)),
},
instructions: vec![Instruction::new_with_bincode(
Pubkey::new_unique(),
&["MyData"],
vec![],
)],
kind: SubscriptionKind::Persistent,
active_commits: 0..=u64::MAX,
};
let instruction = SubscriberInstruction::update(signer, Nonce::default(), handler.clone());
assert_eq!(
instruction.data,
bincode::serialize(&SubscriberInstruction::Update {
nonce: Nonce::default(),
handler
})
.unwrap()
);
assert_eq!(instruction.program_id, crate::ID);
let subscription_data_account = derive_subscription_address(signer, Nonce::default());
assert_eq!(
instruction.accounts,
vec![
AccountMeta::new(signer, true),
AccountMeta::new(subscription_data_account, false),
AccountMeta::new_readonly(system_program::id(), false),
]
)
}
#[test]
fn test_one_shot() {
let signer = Pubkey::new_unique();
let mut handler = Subscription::new(
signer,
Predicate {
topic: String::from("MySubscription"),
event_account: Some(Pubkey::new_unique()),
timestamp_range: None,
},
vec![Instruction::new_with_bincode(
Pubkey::new_unique(),
&["MyData"],
vec![],
)],
SubscriptionKind::OneShot,
0..=u64::MAX,
);
let instruction =
SubscriberInstruction::subscribe_to(signer, Nonce::default(), handler.clone());
handler.instructions.push(Instruction::new_with_bincode(
crate::ID,
&SubscriberInstruction::Destroy {
nonce: Nonce::default(),
},
vec![
AccountMeta::new(signer, true),
AccountMeta::new(derive_subscription_address(signer, Nonce::default()), false),
],
));
assert_eq!(
instruction.data,
bincode::serialize(&SubscriberInstruction::Subscribe {
nonce: Nonce::default(),
handler,
})
.unwrap()
);
assert_eq!(instruction.program_id, crate::ID);
let subscription_data_account = derive_subscription_address(signer, Nonce::default());
assert_eq!(
instruction.accounts,
vec![
AccountMeta::new(signer, true),
AccountMeta::new(subscription_data_account, false),
AccountMeta::new_readonly(system_program::id(), false),
]
)
}
#[test]
fn test_one_shot_with_timestamp_range() {
let signer = Pubkey::new_unique();
let mut handler = Subscription::new(
signer,
Predicate::new_with_timestamp_range(1337..1338),
vec![Instruction::new_with_bincode(
Pubkey::new_unique(),
&["MyData"],
vec![],
)],
SubscriptionKind::OneShot,
0..=u64::MAX,
);
let instruction =
SubscriberInstruction::subscribe_to(signer, Nonce::default(), handler.clone());
handler.instructions.push(Instruction::new_with_bincode(
crate::ID,
&SubscriberInstruction::Destroy {
nonce: Nonce::default(),
},
vec![
AccountMeta::new(signer, true),
AccountMeta::new(derive_subscription_address(signer, Nonce::default()), false),
],
));
assert_eq!(
instruction.data,
bincode::serialize(&SubscriberInstruction::Subscribe {
nonce: Nonce::default(),
handler,
})
.unwrap()
);
assert_eq!(instruction.program_id, crate::ID);
let subscription_data_account = derive_subscription_address(signer, Nonce::default());
assert_eq!(
instruction.accounts,
vec![
AccountMeta::new(signer, true),
AccountMeta::new(subscription_data_account, false),
AccountMeta::new_readonly(system_program::id(), false),
]
)
}
}