Mōdsefa.Donate

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 actual Active value inside the generated validator script.
  • PlutusTx.Eq.Eq: This provides a way to compare FeedStatus values on-chain. It's essential for the validator logic, for example, to check that the feedStatus of a state instance being updated is correctly changed from Active to Archived.

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" FeedData

With 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 = 'OnlyByProperty
  • stateIdentifier: 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, every FeedDataState instance will hold a token with the name "FeedData".
  • AllowedRefStrategy: This defines how we can reference state instances in our actions.
    • FeedConfigState is 'OnlyAsUnique', which enforces that only one configuration instance can exist for our application. We can refer to it simply as "the only instance."
    • FeedDataState is '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 = SingleInstance

Here's what this defines:

  • ManagedStates: This connects our FeedValidator to the FeedConfigState and FeedDataState, making it responsible for them.
  • Params: This declares that the validator script is parameterized by a TxOutRef which we've labeled "bootstrapUtxo". This UTxO will be spent during initialization to create a unique instance of our Feed App.
  • ValidatorInstanceType: By setting this to SingleInstance, we declare that our application can only have one instance of this FeedValidator.

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.