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

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 FeedDataState

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 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 = 'OnlyByProperty

Let's break this down:

  • DatumName: A type-level string that names the concrete datum type Modsefa will generate. FeedConfigState will generate a datum named FeedConfig.
  • 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.
    • FeedConfigState is 'OnlyAsUnique, which enforces that only one configuration instance can exist. We refer to it as "the only instance" (TypedTheOnlyInstance).
    • FeedDataState is '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 = 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.