diff --git a/Cargo.lock b/Cargo.lock index 24fc6b5e..a9556fda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3677,6 +3677,17 @@ dependencies = [ "nom", ] +[[package]] +name = "sablier-macros" +version = "1.0.0-alpha.3" +dependencies = [ + "proc-macro2", + "quote", + "sablier-utils", + "solana-sdk", + "syn 2.0.69", +] + [[package]] name = "sablier-network-program" version = "1.0.0-alpha.3" @@ -3751,6 +3762,7 @@ version = "1.0.0-alpha.3" dependencies = [ "anchor-lang", "base64 0.21.7", + "sablier-macros", "serde", "static-pubkey", ] diff --git a/Cargo.toml b/Cargo.toml index 7ed92a35..058a929d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,3 +71,6 @@ regex = "1.7.1" async-trait = "0.1.64" rustc_version = "0.4.0" bytemuck = "1.4.0" +syn = "2.0" +quote = "1.0" +proc-macro2 = "1.0" diff --git a/programs/thread/src/constants.rs b/programs/thread/src/constants.rs index 4c5c6384..e4625342 100644 --- a/programs/thread/src/constants.rs +++ b/programs/thread/src/constants.rs @@ -7,10 +7,6 @@ pub const SEED_THREAD: &[u8] = b"thread"; #[constant] pub const THREAD_MINIMUM_FEE: u64 = 1000; -/// Static space for next_instruction field. -#[constant] -pub const NEXT_INSTRUCTION_SIZE: usize = 1232; - /// The ID of the pool workers must be a member of to collect fees. #[constant] pub const POOL_ID: u64 = 0; diff --git a/programs/thread/src/instructions/thread_create.rs b/programs/thread/src/instructions/thread_create.rs index 51db658f..7f7d5b20 100644 --- a/programs/thread/src/instructions/thread_create.rs +++ b/programs/thread/src/instructions/thread_create.rs @@ -1,5 +1,3 @@ -use std::mem::size_of; - use anchor_lang::{ prelude::*, system_program::{transfer, Transfer}, @@ -33,14 +31,7 @@ pub struct ThreadCreate<'info> { ], bump, payer= payer, - space = [ - 8, - size_of::(), - id.len(), - instructions.try_to_vec()?.len(), - trigger.try_to_vec()?.len(), - NEXT_INSTRUCTION_SIZE, - ].iter().sum() + space = Thread::min_space(&instructions)? )] pub thread: Account<'info, Thread>, } @@ -69,7 +60,6 @@ pub fn handler( thread.id = id; thread.domain = domain; thread.instructions = instructions; - thread.name = String::new(); thread.next_instruction = None; thread.paused = false; thread.rate_limit = u64::MAX; diff --git a/programs/thread/src/state/thread.rs b/programs/thread/src/state/thread.rs index f7a160a7..0989d6d3 100644 --- a/programs/thread/src/state/thread.rs +++ b/programs/thread/src/state/thread.rs @@ -1,15 +1,13 @@ -use std::mem::size_of; - use anchor_lang::{prelude::*, AnchorDeserialize, AnchorSerialize}; use sablier_utils::{ account::AccountInfoExt, thread::{ClockData, SerializableInstruction, Trigger}, + MinSpace, Space, }; -use crate::constants::{NEXT_INSTRUCTION_SIZE, SEED_THREAD}; +use crate::constants::SEED_THREAD; /// Tracks the current state of a transaction thread on Solana. -// TODO Wait for the next version of Anchor to implement InitSpace macro #[account] #[derive(Debug)] pub struct Thread { @@ -28,8 +26,6 @@ pub struct Thread { pub id: Vec, /// The instructions to be executed. pub instructions: Vec, - /// The name of the thread. - pub name: String, /// The next instruction to be executed. pub next_instruction: Option, /// Whether or not the thread is currently paused. @@ -73,6 +69,29 @@ pub trait ThreadAccount { fn realloc_account(&mut self) -> Result<()>; } +impl Thread { + pub fn min_space(instructions: &[SerializableInstruction]) -> Result { + let ins_number = instructions.len(); + let ins_space = instructions.try_to_vec()?.len(); + + Ok( + 8 + + Pubkey::MIN_SPACE // authority + + u8::MIN_SPACE // bump + + ClockData::MIN_SPACE // created_at + + (1 + 4 + 6) // domain + + >::MIN_SPACE // exec_context + + u64::MIN_SPACE // fee + + (4 + 32) // id + + (4 + ins_space) // instructions + + (1 + ins_space / ins_number) // next_instruction + + bool::MIN_SPACE // paused + + u64::MIN_SPACE // rate_limit + + Trigger::MIN_SPACE, // trigger + ) + } +} + impl ThreadAccount for Account<'_, Thread> { fn pubkey(&self) -> Pubkey { Thread::pubkey(self.authority, self.id.clone(), self.domain.clone()) @@ -80,23 +99,15 @@ impl ThreadAccount for Account<'_, Thread> { fn realloc_account(&mut self) -> Result<()> { // Realloc memory for the thread account - let data_len = [ - 8, - size_of::(), - self.id.len(), - self.instructions.try_to_vec()?.len(), - self.trigger.try_to_vec()?.len(), - NEXT_INSTRUCTION_SIZE, - ] - .iter() - .sum(); + let data_len = 8 + self.try_to_vec()?.len(); + self.realloc(data_len, false)?; Ok(()) } } /// The execution context of a particular transaction thread. -#[derive(AnchorDeserialize, AnchorSerialize, InitSpace, Clone, Copy, Debug, PartialEq, Eq)] +#[derive(AnchorDeserialize, AnchorSerialize, MinSpace, Clone, Copy, Debug, PartialEq, Eq)] pub struct ExecContext { /// Index of the next instruction to be executed. pub exec_index: u64, @@ -116,7 +127,7 @@ pub struct ExecContext { } /// The event which allowed a particular transaction thread to be triggered. -#[derive(AnchorDeserialize, AnchorSerialize, InitSpace, Clone, Copy, Debug, PartialEq, Eq)] +#[derive(AnchorDeserialize, AnchorSerialize, MinSpace, Clone, Copy, Debug, PartialEq, Eq)] pub enum TriggerContext { /// A running hash of the observed account data. Account { diff --git a/utils/Cargo.toml b/utils/Cargo.toml index 234ae1df..c81f50a9 100644 --- a/utils/Cargo.toml +++ b/utils/Cargo.toml @@ -18,3 +18,4 @@ anchor-lang.workspace = true base64.workspace = true serde = { workspace = true, features = ["derive"] } static-pubkey.workspace = true +sablier-macros = { path = "./macros" } diff --git a/utils/macros/Cargo.toml b/utils/macros/Cargo.toml new file mode 100644 index 00000000..455b7cc0 --- /dev/null +++ b/utils/macros/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "sablier-macros" +version.workspace = true +description = "Tools for building blocks on Solana" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +documentation.workspace = true +readme = "./README.md" +keywords.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +proc-macro = true + +[dependencies] +syn = { workspace = true, features = ["full"] } +proc-macro2.workspace = true +quote.workspace = true + +[dev-dependencies] +sablier-utils.workspace = true +solana-sdk.workspace = true diff --git a/utils/macros/src/lib.rs b/utils/macros/src/lib.rs new file mode 100644 index 00000000..7384658c --- /dev/null +++ b/utils/macros/src/lib.rs @@ -0,0 +1,178 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::{quote, quote_spanned}; +use space::{Arg, Args}; +use syn::{ + parse_macro_input, parse_quote, spanned::Spanned, AngleBracketedGenericArguments, DataEnum, + DataStruct, DeriveInput, Expr, Field, Fields, GenericParam, Generics, PathArguments, Type, + TypeArray, TypePath, Variant, +}; + +mod space; + +#[proc_macro_derive(MinSpace, attributes(max_len, raw_space, internal))] +pub fn derive_min_space(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + let name = input.ident; + let generics = add_trait_bounds(input.generics); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let token = match input.data { + syn::Data::Struct(DataStruct { fields, .. }) => { + let len_expr = get_len_expr_from_fields(fields); + + quote! { + #[automatically_derived] + impl #impl_generics sablier_utils::Space for #name #ty_generics #where_clause { + const MIN_SPACE: usize = #len_expr; + } + } + } + syn::Data::Enum(DataEnum { variants, .. }) => { + let variants = variants + .into_iter() + .map(|Variant { fields, .. }| get_len_expr_from_fields(fields)); + let max = gen_max(variants); + + quote! { + #[automatically_derived] + impl sablier_utils::Space for #name { + const MIN_SPACE: usize = 1 + #max; + } + } + } + syn::Data::Union(_) => { + quote_spanned! { name.span() => compile_error!("Union non implemented.") } + } + }; + + TokenStream::from(token) +} + +// Add a bound `T: Space` to every type parameter T. +fn add_trait_bounds(mut generics: Generics) -> Generics { + for param in &mut generics.params { + if let GenericParam::Type(ref mut type_param) = *param { + type_param.bounds.push(parse_quote!(sablier_utils::Space)); + } + } + generics +} + +fn gen_max>(mut iter: T) -> TokenStream2 { + if let Some(item) = iter.next() { + let next_item = gen_max(iter); + quote!(sablier_utils::space::max(#item, #next_item)) + } else { + quote!(0) + } +} + +fn get_len_expr_from_fields(fields: Fields) -> TokenStream2 { + let len = fields.into_iter().map(|f| match TyLen::try_from(f) { + Ok(TyLen(len)) => quote!(#len), + Err(err) => err.into_compile_error(), + }); + + quote!(0 #(+ #len)*) +} + +fn expr_from_ty(value: Type, args: &mut Vec) -> syn::Result { + let current_arg = args.pop(); + + let arg = match current_arg { + Some(Arg { is_raw, ref value }) => { + if is_raw { + return Ok(parse_quote!(#value)); + } else { + Some(value) + } + } + None => None, + }; + + match value { + Type::Array(TypeArray { elem, len, .. }) => { + let inner_ty = expr_from_ty(*elem, args)?; + + Ok(parse_quote!((#len * #inner_ty))) + } + Type::Path(TypePath { ref path, .. }) => { + let Some(segment) = path.segments.last() else { + return Err(syn::Error::new(value.span(), "Invalid path type.")); + }; + let ident = &segment.ident; + + match ident.to_string().as_str() { + "String" => { + let Some(arg_value) = arg else { + return Err(syn::Error::new(ident.span(), "No max_len specified.")); + }; + + Ok(parse_quote!((4 + #arg_value))) + } + "Vec" => { + let Some(arg_value) = arg else { + return Err(syn::Error::new(ident.span(), "No max_len specified.")); + }; + + let new_ty = parse_first_arg(&segment.arguments)?; + let new_len = expr_from_ty(new_ty, args)?; + + Ok(parse_quote!((4 + #new_len * #arg_value))) + } + _ => Ok(parse_quote!(<#value as sablier_utils::Space>::MIN_SPACE)), + } + } + _ => Ok(parse_quote!(<#value as sablier_utils::Space>::MIN_SPACE)), + } +} + +fn parse_first_arg(path_args: &PathArguments) -> syn::Result { + let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) = path_args + else { + return Err(syn::Error::new( + path_args.span(), + "Invalid type of arguments.", + )); + }; + + match &args[0] { + syn::GenericArgument::Type(ty) => Ok(ty.to_owned()), + _ => Err(syn::Error::new( + path_args.span(), + "The first argument is not a type.", + )), + } +} + +struct TyLen(Expr); + +impl TryFrom for TyLen { + type Error = syn::Error; + + fn try_from(value: Field) -> Result { + let Some(name) = value.ident else { + return Err(syn::Error::new(value.span(), "Tuple field is not allowed.")); + }; + + let mut attr_args = value + .attrs + .into_iter() + .filter_map(|a| Args::try_from(a).ok()); + + let args = attr_args.by_ref().take(1).next(); + + if attr_args.next().is_some() { + return Err(syn::Error::new( + name.span(), + "max_len and raw_space cannot be used at the same time.", + )); + } + + let expr = expr_from_ty(value.ty, &mut args.unwrap_or_default())?; + + Ok(TyLen(expr)) + } +} diff --git a/utils/macros/src/space/arg.rs b/utils/macros/src/space/arg.rs new file mode 100644 index 00000000..0a214582 --- /dev/null +++ b/utils/macros/src/space/arg.rs @@ -0,0 +1,162 @@ +use std::ops::{Deref, DerefMut}; + +use quote::ToTokens; +use syn::{ + parenthesized, parse::Parse, parse2, punctuated::Punctuated, spanned::Spanned, token::Paren, + Attribute, Ident, Token, +}; + +use super::Value; + +#[derive(Debug)] +pub struct Arg { + pub is_raw: bool, + pub value: Value, +} + +impl Parse for Arg { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + if input.peek2(Paren) { + let ident: Ident = input.parse()?; + + if ident != "raw_space" { + return Err(syn::Error::new( + input.span(), + "The raw arg must be right like 'raw_space(your value)'", + )); + } + + let content; + parenthesized!(content in input); + + Ok(Arg { + is_raw: true, + value: content.parse()?, + }) + } else { + Ok(Arg { + is_raw: false, + value: input.parse()?, + }) + } + } +} + +#[derive(Default, Debug)] +pub struct Args(pub Vec); + +impl Deref for Args { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Args { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl TryFrom for Args { + type Error = syn::Error; + + fn try_from(value: Attribute) -> Result { + if value.path().is_ident("max_len") { + let ty_lens: Punctuated = value + .meta + .require_list()? + .parse_args_with(Punctuated::parse_terminated)?; + + Ok(Args(ty_lens.into_iter().rev().collect())) + } else if value.path().is_ident("raw_space") { + let value = parse2(value.meta.require_list()?.into_token_stream())?; + + Ok(Args(vec![value])) + } else { + Err(syn::Error::new(value.span(), "Invalid attribute.")) + } + } +} + +#[cfg(test)] +mod tests { + use quote::ToTokens; + use syn::parse_quote; + + use super::*; + + fn stringify_value(v: &Value) -> String { + v.into_token_stream().to_string() + } + + #[test] + fn basic_max_len_attr() { + let attr: Attribute = parse_quote!(#[max_len(10)]); + let args = Args::try_from(attr).unwrap(); + + let arg = &args[0]; + + assert!(!arg.is_raw); + assert_eq!(stringify_value(&arg.value), "10"); + } + + #[test] + fn complex_max_len_attr() { + let attr: Attribute = parse_quote!(#[max_len(10, raw_space(40), LEN, raw_space(LEN))]); + let args = Args::try_from(attr).unwrap(); + + let arg = &args[0]; + assert!(arg.is_raw); + assert_eq!(stringify_value(&arg.value), "(LEN as usize)"); + + let arg = &args[1]; + assert!(!arg.is_raw); + assert_eq!(stringify_value(&arg.value), "(LEN as usize)"); + + let arg = &args[2]; + assert!(arg.is_raw); + assert_eq!(stringify_value(&arg.value), "40"); + + let arg = &args[3]; + assert!(!arg.is_raw); + assert_eq!(stringify_value(&arg.value), "10"); + } + + #[test] + #[should_panic] + fn wrong_max_len_attr() { + let attr: Attribute = parse_quote!(#[max_len(raw_space(raw_space(10)))]); + Args::try_from(attr).unwrap(); + } + + #[test] + fn lit_raw_space_attr() { + let attr: Attribute = parse_quote!(#[raw_space(10)]); + let args = Args::try_from(attr).unwrap(); + + let arg = &args[0]; + + assert!(arg.is_raw); + assert_eq!(stringify_value(&arg.value), "10"); + } + + #[test] + fn const_raw_space_attr() { + let attr: Attribute = parse_quote!(#[raw_space(LEN)]); + let args = Args::try_from(attr).unwrap(); + + let arg = &args[0]; + + assert!(arg.is_raw); + assert_eq!(stringify_value(&arg.value), "(LEN as usize)"); + } + + #[test] + #[should_panic] + fn wrong_raw_space_attr() { + let attr: Attribute = parse_quote!(#[raw_space("Ok")]); + Args::try_from(attr).unwrap(); + } +} diff --git a/utils/macros/src/space/mod.rs b/utils/macros/src/space/mod.rs new file mode 100644 index 00000000..af3a3bd5 --- /dev/null +++ b/utils/macros/src/space/mod.rs @@ -0,0 +1,5 @@ +mod arg; +mod value; + +pub use arg::*; +pub use value::*; diff --git a/utils/macros/src/space/value.rs b/utils/macros/src/space/value.rs new file mode 100644 index 00000000..8eea4d4f --- /dev/null +++ b/utils/macros/src/space/value.rs @@ -0,0 +1,56 @@ +use proc_macro2::TokenStream as TokenStream2; +use quote::{quote, ToTokens}; +use syn::{parse::Parse, Ident, LitInt}; + +#[derive(Debug)] +pub enum Value { + Lit(LitInt), + Ident(Ident), +} + +impl Parse for Value { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let lookahead = input.lookahead1(); + if lookahead.peek(Ident) { + input.parse().map(Value::Ident) + } else if lookahead.peek(LitInt) { + input.parse().map(Value::Lit) + } else { + Err(lookahead.error()) + } + } +} + +impl ToTokens for Value { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let expanded = match self { + Value::Lit(lit) => quote!(#lit), + Value::Ident(ident) => quote!((#ident as usize)), + }; + + expanded.to_tokens(tokens); + } +} + +#[cfg(test)] +mod tests { + use syn::parse_quote; + + use super::*; + + #[test] + fn const_value() { + let val: Value = parse_quote!(LEN); + let token = val.into_token_stream().to_string(); + + assert_eq!(token, "(LEN as usize)"); + } + + #[test] + fn num_value() { + let val: Value = parse_quote!(123); + let token = val.into_token_stream().to_string(); + + assert_eq!(token, "123"); + } +} diff --git a/utils/macros/tests/space.rs b/utils/macros/tests/space.rs new file mode 100644 index 00000000..94eabe26 --- /dev/null +++ b/utils/macros/tests/space.rs @@ -0,0 +1,157 @@ +use sablier_utils::MinSpace; +use sablier_utils::Space; +use solana_sdk::pubkey::Pubkey; + +mod inside_mod { + use sablier_utils::MinSpace; + + #[derive(MinSpace)] + pub struct Data { + pub data: u64, + } +} + +#[derive(MinSpace)] +pub enum TestBasicEnum { + Basic1, + Basic2 { + test_u8: u8, + }, + Basic3 { + test_u16: u16, + }, + Basic4 { + #[max_len(10)] + test_vec: Vec, + }, +} + +#[derive(MinSpace)] +pub struct TestEmptyAccount {} + +#[derive(MinSpace)] +pub struct TestBasicVarAccount { + pub test_u8: u8, + pub test_u16: u16, + pub test_u32: u32, + pub test_u64: u64, + pub test_u128: u128, +} + +#[derive(MinSpace)] +pub struct TestComplexeVarAccount { + pub test_key: Pubkey, + #[max_len(10)] + pub test_vec: Vec, + #[max_len(10)] + pub test_string: String, + pub test_option: Option, +} + +#[derive(MinSpace)] +pub struct TestNonAccountStruct { + pub test_bool: bool, +} + +#[derive(MinSpace)] +pub struct TestZeroCopyStruct { + pub test_array: [u8; 8], + pub test_u32: u32, +} + +#[derive(MinSpace)] +pub struct ChildStruct { + #[max_len(10)] + pub test_string: String, +} + +#[derive(MinSpace)] +pub struct TestNestedStruct { + pub test_struct: ChildStruct, + pub test_enum: TestBasicEnum, +} + +#[derive(MinSpace)] +pub struct TestMatrixStruct { + #[max_len(2, 4)] + pub test_matrix: Vec>, +} + +#[derive(MinSpace)] +pub struct TestFullPath { + pub test_option_path: Option, + pub test_path: inside_mod::Data, +} + +const MAX_LEN: u8 = 10; + +#[derive(MinSpace)] +pub struct TestConst { + #[max_len(MAX_LEN)] + pub test_string: String, + pub test_array: [u8; MAX_LEN as usize], +} + +#[derive(MinSpace)] +pub struct TestRaw { + #[raw_space(100)] + pub test_string: String, + #[max_len(2, raw_space(10))] + pub test_matrix: Vec>, +} + +#[test] +fn test_empty_struct() { + assert_eq!(TestEmptyAccount::MIN_SPACE, 0); +} + +#[test] +fn test_basic_struct() { + assert_eq!(TestBasicVarAccount::MIN_SPACE, 1 + 2 + 4 + 8 + 16); +} + +#[test] +fn test_complexe_struct() { + assert_eq!( + TestComplexeVarAccount::MIN_SPACE, + 32 + 4 + 10 + (4 + 10) + 3 + ) +} + +#[test] +fn test_zero_copy_struct() { + assert_eq!(TestZeroCopyStruct::MIN_SPACE, 8 + 4) +} + +#[test] +fn test_basic_enum() { + assert_eq!(TestBasicEnum::MIN_SPACE, 1 + 14); +} + +#[test] +fn test_nested_struct() { + assert_eq!( + TestNestedStruct::MIN_SPACE, + ChildStruct::MIN_SPACE + TestBasicEnum::MIN_SPACE + ) +} + +#[test] +fn test_matrix_struct() { + assert_eq!(TestMatrixStruct::MIN_SPACE, 4 + (2 * (4 + 4))) +} + +#[test] +fn test_full_path() { + assert_eq!(TestFullPath::MIN_SPACE, 8 + 9) +} + +#[test] +fn test_const() { + assert_eq!(TestConst::MIN_SPACE, (4 + 10) + 10) +} + +#[test] +fn test_raw() { + assert_eq!(TestRaw::MIN_SPACE, 100 + 4 + 2 * 10); +} diff --git a/utils/src/lib.rs b/utils/src/lib.rs index bff23a93..109c0b9f 100644 --- a/utils/src/lib.rs +++ b/utils/src/lib.rs @@ -1,6 +1,7 @@ pub mod account; pub mod explorer; pub mod pubkey; +pub mod space; pub mod thread; use std::fmt::{Debug, Display, Formatter}; @@ -8,6 +9,9 @@ use std::fmt::{Debug, Display, Formatter}; use anchor_lang::{prelude::Pubkey, prelude::*, AnchorDeserialize}; use base64::{engine::general_purpose::STANDARD, Engine}; +pub use sablier_macros::MinSpace; +pub use space::Space; + /// Crate build information #[derive(AnchorDeserialize, AnchorSerialize, Clone, Debug)] pub struct CrateInfo { diff --git a/utils/src/space.rs b/utils/src/space.rs new file mode 100644 index 00000000..413304fb --- /dev/null +++ b/utils/src/space.rs @@ -0,0 +1,36 @@ +use anchor_lang::solana_program::pubkey::Pubkey; + +/// Defines the space of an account for initialization. +pub trait Space { + const MIN_SPACE: usize; +} + +macro_rules! impl_min_space { + ($ty:ident => $space:literal) => { + impl Space for $ty { + const MIN_SPACE: usize = $space; + } + }; + (($($ty:ident),+) => $space:literal) => { + $( + impl_min_space!($ty => $space); + )+ + + }; +} + +impl_min_space!((i8, u8, bool) => 1); +impl_min_space!((i16, u16) => 2); +impl_min_space!((i32, u32, f32) => 4); +impl_min_space!((i64, u64, f64) => 8); +impl_min_space!((i128, u128) => 16); +impl_min_space!(Pubkey => 32); + +impl Space for Option { + const MIN_SPACE: usize = 1 + T::MIN_SPACE; +} + +#[doc(hidden)] +pub const fn max(a: usize, b: usize) -> usize { + [a, b][(a < b) as usize] +} diff --git a/utils/src/thread.rs b/utils/src/thread.rs index c64effdc..bbfb41bd 100644 --- a/utils/src/thread.rs +++ b/utils/src/thread.rs @@ -1,5 +1,3 @@ -use std::{convert::TryFrom, fmt::Debug, hash::Hash}; - use anchor_lang::{ prelude::borsh::BorshSchema, prelude::Pubkey, @@ -7,14 +5,18 @@ use anchor_lang::{ solana_program::{self, instruction::Instruction}, AnchorDeserialize, }; +use sablier_macros::MinSpace; use serde::{Deserialize, Serialize}; use static_pubkey::static_pubkey; +use std::{convert::TryFrom, fmt::Debug, hash::Hash}; /// The stand-in pubkey for delegating a payer address to a worker. All workers are re-imbursed by the user for lamports spent during this delegation. pub static PAYER_PUBKEY: Pubkey = static_pubkey!("Sab1ierPayer1111111111111111111111111111111"); +extern crate self as sablier_utils; + /// The clock object, representing a specific moment in time recorded by a Solana cluster. -#[derive(AnchorDeserialize, AnchorSerialize, InitSpace, BorshSchema, Clone, Debug, PartialEq)] +#[derive(AnchorDeserialize, AnchorSerialize, MinSpace, BorshSchema, Clone, Debug, PartialEq)] pub struct ClockData { /// The current slot. pub slot: u64, @@ -45,7 +47,7 @@ impl TryFrom> for ClockData { } /// The triggering conditions of a thread. -#[derive(AnchorDeserialize, AnchorSerialize, Debug, Clone, PartialEq)] +#[derive(AnchorDeserialize, AnchorSerialize, MinSpace, Debug, Clone, PartialEq)] pub enum Trigger { /// Allows a thread to be kicked off whenever the data of an account changes. Account { @@ -60,6 +62,7 @@ pub enum Trigger { /// Allows a thread to be kicked off according to a one-time or recurring schedule. Cron { /// The schedule in cron syntax. Value must be parsable by the `sablier_cron` package. + #[max_len(32)] schedule: String, /// Boolean value indicating whether triggering moments may be skipped if they are missed (e.g. due to network downtime). @@ -92,7 +95,7 @@ pub enum Trigger { /// Operators for describing how to compare two values to one another. #[repr(u8)] -#[derive(AnchorDeserialize, AnchorSerialize, Clone, Debug, Eq, PartialEq, Hash)] +#[derive(AnchorDeserialize, AnchorSerialize, MinSpace, Clone, Debug, Eq, PartialEq, Hash)] pub enum Equality { GreaterThanOrEqual, LessThanOrEqual,