Defining the States and Validator
Tutorial Part 2: Defining the States and Validator
Just like with any application, before we define what it does, we need to define the data it will manage. For our Feed App, this process is straightforward. We will define our on-chain data structures as Haskell types and then specify the single validator that will govern them.
All the code for this part can be found in examples/Modsefa/Examples/Feed/Types.hs and examples/Modsefa/Examples/Feed/Validators.hs.
Step 1: Define Your Data Types
First, we define any custom data types that our states will use. In this case, we need an enum to track whether a feed entry is Active or Archived. Modsefa uses Template Haskell (makeLift and makeIsDataIndexed) to automatically handle all the Plutus Data conversions for us.
Next, instead of writing full data declarations for our on-chain states, we simply define empty data types to act as type-level tags. FeedConfigState and FeedDataState will represent our two state types.
-- Data Types
-- | Status of a feed entry (active or archived).
data FeedStatus = Archived | Active
deriving (Eq, Generic, Show)
PlutusTx.makeLift ''FeedStatus
PlutusTx.makeIsDataIndexed ''FeedStatus [('Archived, 0), ('Active, 1)]
-- Provide an instance for resolving 'EnumValue' in specifications.
instance FromEnumValue FeedStatus
-- Provide an on-chain Eq instance using PlutusTx's Data comparison.
instance PlutusTx.Eq.Eq FeedStatus where
(==) a b = equalsData (PlutusTx.toBuiltinData a) (PlutusTx.toBuiltinData b)
-- State Tags
-- | Type-level tag for the feed's singleton configuration state.
data FeedConfigState
-- | Type-level tag for the feed's data entries (of which there can be many).
data FeedDataStateYou'll notice that FeedStatus includes two special instances: FromEnumValue and PlutusTx.Eq.Eq. These are important for connecting our off-chain specification to the on-chain validator:
FromEnumValue: This allows Modsefa to translate a type-level string in our action specification (e.g.,'EnumValue FeedStatus "Active") into an actualActivevalue inside the generated validator script.PlutusTx.Eq.Eq: This provides a way to compareFeedStatusvalues on-chain. It's essential for the validator logic, for example, to check that thefeedStatusof a state instance being updated is correctly changed fromActivetoArchived.
Step 2: Define Your State Specifications
With our state tags defined, we now implement the StateSpec typeclass for each one. This is a crucial step that tells Modsefa everything it needs to know to generate the on-chain datum type and manage its instances.
-- | Specification for the 'FeedConfigState'.
instance StateSpec FeedConfigState where
type DatumName FeedConfigState = "FeedConfig"
type DatumFields FeedConfigState =
'[ '("feedName", BuiltinByteString)
, '("feedOwner", PubKeyHash)
]
-- | Identified by a unique "FeedConfig" token minted by its own validator.
type Identifier FeedConfigState = 'TokenIdentifiedSpec 'OwnPolicySpec "FeedConfig" 1
-- | This is a singleton state; it can only be referenced as 'TypedTheOnlyInstance'.
type Strategy FeedConfigState = 'OnlyAsUnique
instance StateSpec FeedDataState where
type DatumName FeedDataState = "FeedData"
type DatumFields FeedDataState =
'[ '("feedData", BuiltinByteString)
, '("feedStatus", FeedStatus)
]
-- | Identified by a unique "FeedData" token minted by its own validator.
type Identifier FeedDataState = 'TokenIdentifiedSpec 'OwnPolicySpec "FeedData" 1
-- | Multiple instances can exist; they must be referenced by their properties
-- | (e.g., finding the unique one where "feedStatus" is "Active").
type Strategy FeedDataState = 'OnlyByPropertyLet's break this down:
DatumName: A type-level string that names the concrete datum type Modsefa will generate.FeedConfigStatewill generate a datum namedFeedConfig.DatumFields: A type-level list of(Symbol, Type)tuples. This defines the record fields for the generated datum type.Identifier: This specifies how each state instance is identified on-chain. Here, both are identified by a unique token ('TokenIdentifiedSpec) minted by the validator itself ('OwnPolicySpec).Strategy: This defines how we can reference state instances in our actions.FeedConfigStateis'OnlyAsUnique, which enforces that only one configuration instance can exist. We refer to it as "the only instance" (TypedTheOnlyInstance).FeedDataStateis'OnlyByProperty, allowing for many instances that we can query based on their datum fields (e.g., "find the data instance where the status is 'Active'").
Step 3: Specify the Validator
Because this is a simple application, we only need one validator to manage both of our state types. We define it by creating an empty data type to act as a tag, and then implementing the ValidatorSpec typeclass for it.
data FeedValidator
instance ValidatorSpec FeedValidator where
type Params FeedValidator = '[ '("bootstrapUtxo", TxOutRef) ]
type ManagedStates FeedValidator = '[FeedConfigState, FeedDataState]
type ValidatorAppName FeedValidator = "FeedValidator"
type ValidatorPlutusVersion FeedValidator = 'PlutusV3
type ValidatorInstanceType FeedValidator = SingleInstanceHere's what this defines:
ManagedStates: This connects ourFeedValidatorto theFeedConfigStateandFeedDataState, making it responsible for them.Params: This declares that the validator script is parameterized by aTxOutRefwhich we've labeled"bootstrapUtxo". This UTxO will be spent during initialization to create a unique instance of our Feed App.ValidatorInstanceType: By setting this toSingleInstance, we declare that our application can only have one instance of thisFeedValidator.
We have now defined all the data structures and the single validator that will protect them. The logic of how this validator behaves will be automatically generated from the actions we define in the next part.