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
We need three Haskell types to represent our on-chain data: one for the feed's overall configuration, one for the content of a feed entry, and an enum to track the status of each entry. Modsefa uses Template Haskell (makeLift and makeIsDataIndexed) to automatically handle all the Plutus Data conversions for us.
data FeedConfig = FeedConfig
{ feedName :: BuiltinByteString
, feedOwner :: PubKeyHash
} deriving (Eq, Generic, Show)
PlutusTx.makeLift ''FeedConfig
PlutusTx.makeIsDataIndexed ''FeedConfig [('FeedConfig, 0)]
data FeedStatus = Archived | Active
deriving (Eq, Generic, Show)
PlutusTx.makeLift ''FeedStatus
PlutusTx.makeIsDataIndexed ''FeedStatus [('Archived, 0), ('Active, 1)]
instance FromEnumValue FeedStatus
instance PlutusTx.Eq.Eq FeedStatus where
(==) a b = equalsData (PlutusTx.toBuiltinData a) (PlutusTx.toBuiltinData b)
data FeedData = FeedData
{ feedData :: BuiltinByteString
, feedStatus :: FeedStatus
} deriving (Eq, Generic, Show)
PlutusTx.makeLift ''FeedData
PlutusTx.makeIsDataIndexed ''FeedData [('FeedData, 0)]You'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 Types
Next, we promote these data types into StateTypes. This is a type alias that gives a type-level name to each of our data structures.
type FeedConfigState = 'ST "FeedConfig" FeedConfig
type FeedDataState = 'ST "FeedData" FeedDataWith our states defined, we now implement the StateRepresentable typeclass. This is a crucial step that tells Modsefa how to identify and manage instances of these states on the blockchain.
instance StateRepresentable FeedConfigState where
stateIdentifier _ = TokenIdentified OwnPolicy "FeedConfig" 1
type AllowedRefStrategy FeedConfigState = 'OnlyAsUnique
instance StateRepresentable FeedDataState where
stateIdentifier _ = TokenIdentified OwnPolicy "FeedData" 1
type AllowedRefStrategy FeedDataState = 'OnlyByPropertystateIdentifier: This specifies that each state instance is identified by a UTxO containing a unique token minted by the validator that manages it (OwnPolicy). For example, everyFeedDataStateinstance will hold a token with the name "FeedData".AllowedRefStrategy: This defines how we can reference state instances in our actions.FeedConfigStateis'OnlyAsUnique', which enforces that only one configuration instance can exist for our application. We can refer to it simply as "the only instance."FeedDataStateis'OnlyByProperty', allowing for many instances that we can query and find based on the data in their datums (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.