soroban_tools/fsm/
impl.rs

1/*
2    Copyright (c) 2023-2024 Frederic Kyung-jin Rezeau (오경진 吳景振)
3
4    This file is part of soroban-kit.
5
6    Licensed under the MIT License, this software is provided "AS IS",
7    no liability assumed. For details, see the LICENSE file in the
8    root directory.
9
10    Author: Fred Kyung-jin Rezeau <fred@litemint.com>
11*/
12
13use core::marker::PhantomData;
14use soroban_sdk::{contracttype, Env, IntoVal, TryFromVal, Val};
15
16// Control state transitions for the state machine.
17pub trait TransitionHandler<K, V>
18where
19    K: Clone + IntoVal<Env, Val> + TryFromVal<Env, Val>,
20    V: Clone + IntoVal<Env, Val> + TryFromVal<Env, Val>,
21{
22    // Called immediately before state validation.
23    // Used to implement guard conditions for the transition (e.g., ledger sequence or time-based guards).
24    fn on_guard(&self, env: &Env, state_machine: &StateMachine<K, V>);
25
26    // Called immediately after state validation iff validation succeeded.
27    // Used to implement the effect from transitioning.
28    fn on_effect(&self, env: &Env, state_machine: &StateMachine<K, V>);
29}
30
31// Generic finite state machine using Soroban storage for state serialization.
32// Support for state concurrency with regions and extended state variables to allow
33// modeling of complex behaviors.
34pub struct StateMachine<'a, K, V>
35where
36    K: 'a + IntoVal<Env, Val> + TryFromVal<Env, Val>,
37    V: IntoVal<Env, Val> + TryFromVal<Env, Val>,
38{
39    region: &'a K,
40    storage_type: StorageType,
41    _data: PhantomData<*const V>,
42}
43
44impl<'a, K, V> StateMachine<'a, K, V>
45where
46    K: Clone + IntoVal<Env, Val> + TryFromVal<Env, Val>,
47    V: Clone + IntoVal<Env, Val> + TryFromVal<Env, Val>,
48{
49    pub fn new(region: &'a K, storage_type: StorageType) -> Self {
50        StateMachine {
51            region,
52            storage_type,
53            _data: PhantomData,
54        }
55    }
56
57    pub fn get_region(&self) -> &'a K {
58        self.region
59    }
60
61    pub fn get_storage_type(&self) -> &StorageType {
62        &self.storage_type
63    }
64
65    pub fn set_state(&self, env: &Env, value: &V) {
66        match self.storage_type {
67            StorageType::Instance => env
68                .storage()
69                .instance()
70                .set(&self.region.into_val(env), value),
71            StorageType::Persistent => env
72                .storage()
73                .persistent()
74                .set(&self.region.into_val(env), value),
75            StorageType::Temporary => env
76                .storage()
77                .temporary()
78                .set(&self.region.into_val(env), value),
79        }
80    }
81
82    pub fn get_state(&self, env: &Env) -> Option<V> {
83        match self.storage_type {
84            StorageType::Instance => env.storage().instance().get(&self.region.into_val(env)),
85            StorageType::Persistent => env.storage().persistent().get(&self.region.into_val(env)),
86            StorageType::Temporary => env.storage().temporary().get(&self.region.into_val(env)),
87        }
88    }
89
90    pub fn remove_state(&self, env: &Env) {
91        match self.storage_type {
92            StorageType::Instance => env.storage().instance().remove(&self.region.into_val(env)),
93            StorageType::Persistent => env.storage().persistent().remove(&self.region.into_val(env)),
94            StorageType::Temporary => env.storage().temporary().remove(&self.region.into_val(env)),
95        }
96    }
97}
98
99#[contracttype]
100#[derive(Debug, Clone, Eq, PartialEq)]
101pub enum StorageType {
102    Instance,
103    Persistent,
104    Temporary,
105}
106
107// Default region if none is specified.
108#[contracttype]
109#[derive(Debug, Clone, Eq, PartialEq)]
110pub enum StateMachineRegion {
111    Default,
112}
113
114// Most of the code here is for pattern matching with variadic, optional parameters
115// and expansion of extended state variable types and regions.
116// The injected code remains minimal despite the syntax verbosity (any rust macro syntax experts?).
117// See @internal arm for state validation logic.
118#[macro_export]
119macro_rules! impl_state_machine {
120    ($instance:expr, $env:expr, $storage_type:expr, $state_enum:ident, $state_variant:ident) => {
121        let state_key = $state_enum::$state_variant;
122        let region_key = $crate::fsm::StateMachineRegion::Default;
123        $crate::impl_state_machine!(@internal $instance, $env, $storage_type, state_key, region_key, $state_enum, $crate::fsm::StateMachineRegion);
124    };
125    ($instance:expr, $env:expr, $storage_type:expr, $state_enum:ident, $state_variant:ident, (),
126        $region_enum:ident, $region_variant:ident, ()) => {
127        let state_key = $state_enum::$state_variant;
128        let region_key = $region_enum::$region_variant;
129        $crate::impl_state_machine!(@internal $instance, $env, $storage_type, state_key, region_key, $state_enum, $region_enum);
130    };
131    ($instance:expr, $env:expr, $storage_type:expr, $state_enum:ident, $state_variant:ident,
132        (), $region_enum:ident, $region_variant:ident, ($($region_tuple_value:expr),+)) => {
133        let state_key = $state_enum::$state_variant;
134        let region_key = $region_enum::$region_variant($($region_tuple_value),*);
135        $crate::impl_state_machine!(@internal $instance, $env, $storage_type, state_key, region_key, $state_enum, $region_enum);
136    };
137    ($instance:expr, $env:expr, $storage_type:expr, $state_enum:ident, $state_variant:ident, ($($state_tuple_value:expr),+)) => {
138        let state_key = $state_enum::$state_variant($($state_tuple_value),*);
139        let region_key = $crate::fsm::StateMachineRegion::Default;
140        $crate::impl_state_machine!(@internal $instance, $env, $storage_type, state_key, region_key, $state_enum, $crate::fsm::StateMachineRegion);
141    };
142    ($instance:expr, $env:expr, $storage_type:expr, $state_enum:ident, $state_variant:ident, ($($state_tuple_value:expr),+),
143        $region_enum:ident, $region_variant:ident, ()) => {
144        let state_key = $state_enum::$state_variant($($state_tuple_value),*);
145        let region_key = $region_enum::$region_variant;
146        $crate::impl_state_machine!(@internal $instance, $env, $storage_type, state_key, region_key, $state_enum, $region_enum);
147    };
148    ($instance:expr, $env:expr, $storage_type:expr, $state_enum:ident, $state_variant:ident,
149        ($($state_tuple_value:expr),+),$region_enum:ident, $region_variant:ident, ($($region_tuple_value:expr),+)) => {
150        let state_key = $state_enum::$state_variant($($state_tuple_value),*);
151        let region_key = $region_enum::$region_variant($($region_tuple_value),*);
152        $crate::impl_state_machine!(@internal $instance, $env, $storage_type, state_key, region_key, $state_enum, $region_enum);
153    };
154    // @internal
155    (@internal $instance:expr, $env:expr, $storage_type:expr, $state_key:expr, $region_key:expr, $state_enum:ty, $region_enum:ty) => {
156        let sm = $crate::fsm::StateMachine::<$region_enum, $state_enum>::new(&$region_key, $storage_type);
157        $instance.on_guard($env, &sm);
158        assert_eq!(sm.get_state(&$env).unwrap(), $state_key);
159        $instance.on_effect($env, &sm);
160    };
161}