Mōdsefa.Donate

Defining the States and Validators

Tutorial Part 2: Defining the States and Validators

Before we can define what our application does, we first need to define the data it operates on. This involves two main steps: defining our on-chain data structures as Haskell types and then specifying the validators that will govern them. All the code for this part can be found in examples/Modsefa/Examples/Subscription/Types.hs and examples/Modsefa/Examples/Subscription/Validators.hs.


Step 1: Define Your State Tags

We define our on-chain data by first creating empty data types. These serve as type-level tags for each state. The concrete data records (like ServiceConfig, PricingTier, etc.) will be generated from these tags.

-- | Type-level tag for the singleton service configuration state.
data ServiceConfigState 
-- | Type-level tag for the pricing tier states.
data PricingTierState 
-- | Type-level tag for the coupon states (mappable for batch operations).
data CouponState
-- | Type-level tag for the customer subscription states.
data CustomerSubscriptionState
-- | Type-level tag for the aggregate ADA treasury state.
data TreasuryAdaState

Step 2: Define Your State Specifications

With our tags defined, we implement the StateSpec class for each one. This instance defines the exact on-chain specification for the state.

-- | Specification for the 'ServiceConfigState'.
instance StateSpec ServiceConfigState where 
  type DatumName ServiceConfigState = "ServiceConfig"
  type DatumFields ServiceConfigState =
    '[ '("serviceConfigName", BuiltinByteString)
     , '("serviceConfigProvider", PubKeyHash) 
     ]
  type Identifier ServiceConfigState = 'TokenIdentifiedSpec 'OwnPolicySpec "ServiceConfig" 1
  type Strategy ServiceConfigState = 'OnlyAsUnique
 
-- | Specification for the 'PricingTierState'.
instance StateSpec PricingTierState where
  type DatumName PricingTierState = "PricingTier"
  type DatumFields PricingTierState = 
    '[ '("pricingTierName", BuiltinByteString)
     , '("pricingTierPrice", Integer)
     , '("pricingTierAssetClass",AssetClass)
     , '("pricingTierBillingPeriod",POSIXTime)
     , '("pricingTierContractLength",POSIXTime) 
     ]
  type Identifier PricingTierState = 'TokenIdentifiedSpec 'OwnPolicySpec "PricingTier" 1 
  type Strategy PricingTierState = 'OnlyByProperty
 
-- | Specification for the 'CouponState'.
instance StateSpec CouponState where
  type DatumName CouponState = "Coupon"
  type DatumFields CouponState = 
    '[ '("couponId", Integer)
     , '("couponBatchId", TxOutRef)
     , '("couponDiscountPercent",Integer) 
     ]
  type Identifier CouponState = 'TokenIdentifiedSpec 'OwnPolicySpec "Coupon" 1
  type Strategy CouponState = 'OnlyByProperty
  type HasMappable CouponState = 'True
 
-- | Specification for the 'CustomerSubscriptionState'.
instance StateSpec CustomerSubscriptionState where 
  type DatumName CustomerSubscriptionState = "CustomerSubscription" 
  type DatumFields CustomerSubscriptionState =
    '[ '("customerSubscriptionPkh", PubKeyHash)
     , '("customerSubscriptionPrice", Integer)
     , '("customerSubscriptionAssetClass",AssetClass)
     , '("customerSubscriptionBillingPeriod",POSIXTime)
     , '("customerSubscriptionContractEndDate",POSIXTime)
     , '("customerSubscriptionPaidThrough",POSIXTime)
     , '("customerSubscriptionServiceProviderValidator",ScriptHash)
     ]
  type Identifier CustomerSubscriptionState = 'TokenIdentifiedSpec 'OwnPolicySpec "CustomerSubscription" 1 
  type Strategy CustomerSubscriptionState = 'OnlyByProperty
 
-- | Specification for the 'TreasuryAdaState'.
instance StateSpec TreasuryAdaState where
  type DatumName TreasuryAdaState = "TreasuryAda"
  type DatumFields TreasuryAdaState = '[]
  type Identifier TreasuryAdaState = 'AggregateAssetSpec AdaPolicy AdaTokenName
  type Strategy TreasuryAdaState = 'NoRef

Let's break this down:

  • DatumName: A type-level string that names the concrete datum type Modsefa will generate (e.g., "ServiceConfig").
  • DatumFields: A type-level list of (Symbol, Type) tuples defining the record fields for the generated datum.
  • Identifier: Defines the on-chain identification mechanism. 'TokenIdentifiedSpec is used for states identified by a unique token, while 'AggregateAssetSpec is used for value-based states like our treasury.
  • Strategy: Defines how actions can reference the state, such as 'OnlyAsUnique for singletons or 'OnlyByProperty for queryable states.
  • HasMappable: Setting this to 'True (as seen on CouponState) enables batch operations on that state type.
  • Aggregate State: The TreasuryAdaState is defined with an empty DatumFields list ('[]) and a 'NoRef strategy, which is the pattern for an aggregate state.

Step 3: Specify the Validators

With our states defined, we now declare the validators that will manage them. We create an empty data type to act as a tag for each validator and then implement the ValidatorSpec typeclass.

data ServiceAndPricingValidator
data CouponValidator
data CustomerValidator
data TreasuryValidator
 
instance ValidatorSpec ServiceAndPricingValidator where
  type Params ServiceAndPricingValidator = '[ '("bootstrapUtxo", TxOutRef) ]
  type ManagedStates ServiceAndPricingValidator = '[ServiceConfigState, PricingTierState]
  type ValidatorAppName ServiceAndPricingValidator = "ServiceAndPricingValidator"
  type ValidatorPlutusVersion ServiceAndPricingValidator = 'PlutusV3
  type ValidatorInstanceType ServiceAndPricingValidator = SingleInstance
 
instance ValidatorSpec CouponValidator where
  type Params CouponValidator = '[ '("serviceValidatorAddress", Address) ]
  type ManagedStates CouponValidator = '[CouponState]
  type ValidatorAppName CouponValidator = "CouponValidator"
  type ValidatorPlutusVersion CouponValidator = 'PlutusV3
  type ValidatorInstanceType CouponValidator = SingleInstance
 
instance ValidatorSpec CustomerValidator where
  type Params CustomerValidator = '[ '("customerPkh", PubKeyHash) ]
  type ManagedStates CustomerValidator = '[CustomerSubscriptionState]
  type ValidatorAppName CustomerValidator = "CustomerValidator"
  type ValidatorPlutusVersion CustomerValidator = 'PlutusV3
  type ValidatorInstanceType CustomerValidator = MultiInstance
 
instance ValidatorSpec TreasuryValidator where
  type Params TreasuryValidator = '[ '("serviceValidatorAddress", Address) ]
  type ManagedStates TreasuryValidator = '[TreasuryAdaState]
  type ValidatorAppName TreasuryValidator = "TreasuryValidator"
  type ValidatorPlutusVersion TreasuryValidator = 'PlutusV3
  type ValidatorInstanceType TreasuryValidator = SingleInstance

Here's what this defines:

  • ManagedStates: Connects each validator to the StateTypes it's responsible for.
  • Params: Defines the data required to parameterize the validator script.
    • ServiceAndPricingValidator is parameterized by a bootstrapUtxo to create a unique instance of the entire service.
    • CustomerValidator is parameterized by a customerPkh, meaning a unique validator instance will be created for each customer.
  • ValidatorInstanceType: Specifies whether the application can have a single (SingleInstance) or multiple (MultiInstance) instances of this validator. This is how we enforce our singleton and multi-instance patterns.

We have now defined all the data structures and the validators that will protect them. The logic of how these validators behave will be automatically generated from the actions we define in a future step.