use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub use hyperstack_idl::snapshot::*;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FieldPath {
pub segments: Vec<String>,
pub offsets: Option<Vec<usize>>,
}
impl FieldPath {
pub fn new(segments: &[&str]) -> Self {
FieldPath {
segments: segments.iter().map(|s| s.to_string()).collect(),
offsets: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Transformation {
HexEncode,
HexDecode,
Base58Encode,
Base58Decode,
ToString,
ToNumber,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PopulationStrategy {
SetOnce,
LastWrite,
Append,
Merge,
Max,
Sum,
Count,
Min,
UniqueCount,
}
fn default_discriminant_size() -> usize {
8
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComputedFieldSpec {
pub target_path: String,
pub expression: ComputedExpr,
pub result_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum ResolverType {
Token,
Url(UrlResolverConfig),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
#[serde(rename_all = "lowercase")]
pub enum HttpMethod {
#[default]
Get,
Post,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum UrlTemplatePart {
Literal(String),
FieldRef(String),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum UrlSource {
FieldPath(String),
Template(Vec<UrlTemplatePart>),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct UrlResolverConfig {
pub url_source: UrlSource,
#[serde(default)]
pub method: HttpMethod,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extract_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolverExtractSpec {
pub target_path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transform: Option<Transformation>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub enum ResolveStrategy {
#[default]
SetOnce,
LastWrite,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolverCondition {
pub field_path: String,
pub op: ComparisonOp,
pub value: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolverSpec {
pub resolver: ResolverType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_value: Option<serde_json::Value>,
#[serde(default)]
pub strategy: ResolveStrategy,
pub extracts: Vec<ResolverExtractSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub condition: Option<ResolverCondition>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schedule_at: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ComputedExpr {
FieldRef {
path: String,
},
UnwrapOr {
expr: Box<ComputedExpr>,
default: serde_json::Value,
},
Binary {
op: BinaryOp,
left: Box<ComputedExpr>,
right: Box<ComputedExpr>,
},
Cast {
expr: Box<ComputedExpr>,
to_type: String,
},
MethodCall {
expr: Box<ComputedExpr>,
method: String,
args: Vec<ComputedExpr>,
},
ResolverComputed {
resolver: String,
method: String,
args: Vec<ComputedExpr>,
},
Literal {
value: serde_json::Value,
},
Paren {
expr: Box<ComputedExpr>,
},
Var {
name: String,
},
Let {
name: String,
value: Box<ComputedExpr>,
body: Box<ComputedExpr>,
},
If {
condition: Box<ComputedExpr>,
then_branch: Box<ComputedExpr>,
else_branch: Box<ComputedExpr>,
},
None,
Some {
value: Box<ComputedExpr>,
},
Slice {
expr: Box<ComputedExpr>,
start: usize,
end: usize,
},
Index {
expr: Box<ComputedExpr>,
index: usize,
},
U64FromLeBytes {
bytes: Box<ComputedExpr>,
},
U64FromBeBytes {
bytes: Box<ComputedExpr>,
},
ByteArray {
bytes: Vec<u8>,
},
Closure {
param: String,
body: Box<ComputedExpr>,
},
Unary {
op: UnaryOp,
expr: Box<ComputedExpr>,
},
JsonToBytes {
expr: Box<ComputedExpr>,
},
ContextSlot,
ContextTimestamp,
Keccak256 {
expr: Box<ComputedExpr>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum BinaryOp {
Add,
Sub,
Mul,
Div,
Mod,
Gt,
Lt,
Gte,
Lte,
Eq,
Ne,
And,
Or,
Xor,
BitAnd,
BitOr,
Shl,
Shr,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum UnaryOp {
Not,
ReverseBits,
}
pub const CURRENT_AST_VERSION: &str = "0.0.1";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializableStreamSpec {
#[serde(default = "default_ast_version")]
pub ast_version: String,
pub state_name: String,
#[serde(default)]
pub program_id: Option<String>,
#[serde(default)]
pub idl: Option<IdlSnapshot>,
pub identity: IdentitySpec,
pub handlers: Vec<SerializableHandlerSpec>,
pub sections: Vec<EntitySection>,
pub field_mappings: BTreeMap<String, FieldTypeInfo>,
pub resolver_hooks: Vec<ResolverHook>,
pub instruction_hooks: Vec<InstructionHook>,
#[serde(default)]
pub resolver_specs: Vec<ResolverSpec>,
#[serde(default)]
pub computed_fields: Vec<String>,
#[serde(default)]
pub computed_field_specs: Vec<ComputedFieldSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_hash: Option<String>,
#[serde(default)]
pub views: Vec<ViewDef>,
}
fn default_ast_version() -> String {
CURRENT_AST_VERSION.to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IdentitySpec {
pub primary_keys: Vec<String>,
pub lookup_indexes: Vec<LookupIndexSpec>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LookupIndexSpec {
pub field_name: String,
pub temporal_field: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializableHandlerSpec {
pub source: SourceSpec,
pub key_resolution: KeyResolutionStrategy,
pub mappings: Vec<SerializableFieldMapping>,
pub conditions: Vec<Condition>,
pub emit: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum KeyResolutionStrategy {
Embedded {
primary_field: FieldPath,
},
Lookup {
primary_field: FieldPath,
},
Computed {
primary_field: FieldPath,
compute_partition: ComputeFunction,
},
TemporalLookup {
lookup_field: FieldPath,
timestamp_field: FieldPath,
index_name: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SourceSpec {
Source {
program_id: Option<String>,
discriminator: Option<Vec<u8>>,
type_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
serialization: Option<IdlSerializationSnapshot>,
#[serde(default)]
is_account: bool,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializableFieldMapping {
pub target_path: String,
pub source: MappingSource,
pub transform: Option<Transformation>,
pub population: PopulationStrategy,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub condition: Option<ConditionExpr>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub when: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stop: Option<String>,
#[serde(default = "default_emit", skip_serializing_if = "is_true")]
pub emit: bool,
}
fn default_emit() -> bool {
true
}
fn is_true(value: &bool) -> bool {
*value
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MappingSource {
FromSource {
path: FieldPath,
default: Option<serde_json::Value>,
transform: Option<Transformation>,
},
Constant(serde_json::Value),
Computed {
inputs: Vec<FieldPath>,
function: ComputeFunction,
},
FromState {
path: String,
},
AsEvent {
fields: Vec<MappingSource>,
},
WholeSource,
AsCapture {
field_transforms: std::collections::BTreeMap<String, Transformation>,
},
FromContext {
field: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ComputeFunction {
Sum,
Concat,
Format(String),
Custom(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Condition {
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntitySection {
pub name: String,
pub fields: Vec<FieldTypeInfo>,
#[serde(default)]
pub is_nested_struct: bool,
#[serde(default)]
pub parent_field: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldTypeInfo {
pub field_name: String,
pub rust_type_name: String,
pub base_type: BaseType,
pub is_optional: bool,
pub is_array: bool,
#[serde(default)]
pub inner_type: Option<String>,
#[serde(default)]
pub source_path: Option<String>,
#[serde(default)]
pub resolved_type: Option<ResolvedStructType>,
#[serde(default = "default_emit", skip_serializing_if = "is_true")]
pub emit: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolvedStructType {
pub type_name: String,
pub fields: Vec<ResolvedField>,
pub is_instruction: bool,
pub is_account: bool,
pub is_event: bool,
#[serde(default)]
pub is_enum: bool,
#[serde(default)]
pub enum_variants: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolvedField {
pub field_name: String,
pub field_type: String,
pub base_type: BaseType,
pub is_optional: bool,
pub is_array: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum BaseType {
Integer,
Float,
String,
Boolean,
Object,
Array,
Binary,
Timestamp,
Pubkey, Any,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolverHook {
pub account_type: String,
pub strategy: ResolverStrategy,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ResolverStrategy {
PdaReverseLookup {
lookup_name: String,
queue_discriminators: Vec<Vec<u8>>,
},
DirectField {
field_path: FieldPath,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstructionHook {
pub instruction_type: String,
pub actions: Vec<HookAction>,
pub lookup_by: Option<FieldPath>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum HookAction {
RegisterPdaMapping {
pda_field: FieldPath,
seed_field: FieldPath,
lookup_name: String,
},
SetField {
target_field: String,
source: MappingSource,
condition: Option<ConditionExpr>,
},
IncrementField {
target_field: String,
increment_by: i64,
condition: Option<ConditionExpr>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConditionExpr {
pub expression: String,
pub parsed: Option<ParsedCondition>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ParsedCondition {
Comparison {
field: FieldPath,
op: ComparisonOp,
value: serde_json::Value,
},
Logical {
op: LogicalOp,
conditions: Vec<ParsedCondition>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ComparisonOp {
Equal,
NotEqual,
GreaterThan,
GreaterThanOrEqual,
LessThan,
LessThanOrEqual,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum LogicalOp {
And,
Or,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum SortOrder {
#[default]
Asc,
Desc,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum CompareOp {
Eq,
Ne,
Gt,
Gte,
Lt,
Lte,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum PredicateValue {
Literal(serde_json::Value),
Dynamic(String),
Field(FieldPath),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Predicate {
Compare {
field: FieldPath,
op: CompareOp,
value: PredicateValue,
},
And(Vec<Predicate>),
Or(Vec<Predicate>),
Not(Box<Predicate>),
Exists { field: FieldPath },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ViewTransform {
Filter { predicate: Predicate },
Sort {
key: FieldPath,
#[serde(default)]
order: SortOrder,
#[serde(skip, default)]
key_span: Option<proc_macro2::Span>,
},
Take { count: usize },
Skip { count: usize },
First,
Last,
MaxBy {
key: FieldPath,
#[serde(skip, default)]
key_span: Option<proc_macro2::Span>,
},
MinBy {
key: FieldPath,
#[serde(skip, default)]
key_span: Option<proc_macro2::Span>,
},
}
impl PartialEq for ViewTransform {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Filter { predicate: l }, Self::Filter { predicate: r }) => l == r,
(
Self::Sort {
key: k1, order: o1, ..
},
Self::Sort {
key: k2, order: o2, ..
},
) => k1 == k2 && o1 == o2,
(Self::Take { count: l }, Self::Take { count: r }) => l == r,
(Self::Skip { count: l }, Self::Skip { count: r }) => l == r,
(Self::First, Self::First) => true,
(Self::Last, Self::Last) => true,
(Self::MaxBy { key: k1, .. }, Self::MaxBy { key: k2, .. }) => k1 == k2,
(Self::MinBy { key: k1, .. }, Self::MinBy { key: k2, .. }) => k1 == k2,
_ => false,
}
}
}
impl Eq for ViewTransform {}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ViewSource {
Entity { name: String },
View { id: String },
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub enum ViewOutput {
#[default]
Collection,
Single,
Keyed { key_field: FieldPath },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ViewDef {
pub id: String,
pub source: ViewSource,
#[serde(default)]
pub pipeline: Vec<ViewTransform>,
#[serde(default)]
pub output: ViewOutput,
}
impl SerializableStreamSpec {
#[allow(dead_code)]
pub fn try_compute_content_hash(&self) -> Result<String, serde_json::Error> {
use sha2::{Digest, Sha256};
let mut spec_for_hash = self.clone();
spec_for_hash.content_hash = None;
let json = serde_json::to_string(&spec_for_hash)?;
let mut hasher = Sha256::new();
hasher.update(json.as_bytes());
let result = hasher.finalize();
Ok(hex::encode(result))
}
#[allow(dead_code)]
pub fn compute_content_hash(&self) -> String {
self.try_compute_content_hash()
.expect("Failed to serialize spec for hashing")
}
#[allow(dead_code)]
pub fn verify_content_hash(&self) -> bool {
match &self.content_hash {
Some(hash) => self
.try_compute_content_hash()
.map(|computed| hash == &computed)
.unwrap_or(false),
None => true, }
}
#[allow(dead_code)]
pub fn try_with_content_hash(mut self) -> Result<Self, serde_json::Error> {
self.content_hash = Some(self.try_compute_content_hash()?);
Ok(self)
}
#[allow(dead_code)]
pub fn with_content_hash(mut self) -> Self {
self.content_hash = Some(self.compute_content_hash());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PdaDefinition {
pub name: String,
pub seeds: Vec<PdaSeedDef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub program_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum PdaSeedDef {
Literal {
value: String,
},
Bytes {
value: Vec<u8>,
},
ArgRef {
arg_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
arg_type: Option<String>,
},
AccountRef {
account_name: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "category", rename_all = "camelCase")]
pub enum AccountResolution {
Signer,
Known {
address: String,
},
PdaRef {
pda_name: String,
},
PdaInline {
seeds: Vec<PdaSeedDef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
program_id: Option<String>,
},
UserProvided,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct InstructionAccountDef {
pub name: String,
#[serde(default)]
pub is_signer: bool,
#[serde(default)]
pub is_writable: bool,
pub resolution: AccountResolution,
#[serde(default)]
pub is_optional: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub docs: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct InstructionArgDef {
pub name: String,
#[serde(rename = "type")]
pub arg_type: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub docs: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct InstructionDef {
pub name: String,
pub discriminator: Vec<u8>,
#[serde(default = "default_discriminant_size")]
pub discriminator_size: usize,
pub accounts: Vec<InstructionAccountDef>,
pub args: Vec<InstructionArgDef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub errors: Vec<IdlErrorSnapshot>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub program_id: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub docs: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializableStackSpec {
#[serde(default = "default_ast_version")]
pub ast_version: String,
pub stack_name: String,
#[serde(default)]
pub program_ids: Vec<String>,
#[serde(default)]
pub idls: Vec<IdlSnapshot>,
pub entities: Vec<SerializableStreamSpec>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub pdas: BTreeMap<String, BTreeMap<String, PdaDefinition>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub instructions: Vec<InstructionDef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_hash: Option<String>,
}
impl SerializableStackSpec {
#[allow(dead_code)]
pub fn try_compute_content_hash(&self) -> Result<String, serde_json::Error> {
use sha2::{Digest, Sha256};
let mut spec_for_hash = self.clone();
spec_for_hash.content_hash = None;
let json = serde_json::to_string(&spec_for_hash)?;
let mut hasher = Sha256::new();
hasher.update(json.as_bytes());
Ok(hex::encode(hasher.finalize()))
}
#[allow(dead_code)]
pub fn compute_content_hash(&self) -> String {
self.try_compute_content_hash()
.expect("Failed to serialize stack spec for hashing")
}
#[allow(dead_code)]
pub fn try_with_content_hash(mut self) -> Result<Self, serde_json::Error> {
self.content_hash = Some(self.try_compute_content_hash()?);
Ok(self)
}
#[allow(dead_code)]
pub fn with_content_hash(mut self) -> Self {
self.content_hash = Some(self.compute_content_hash());
self
}
}