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 TreasuryAdaStateStep 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 = 'NoRefLet'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.'TokenIdentifiedSpecis used for states identified by a unique token, while'AggregateAssetSpecis used for value-based states like our treasury.Strategy: Defines how actions can reference the state, such as'OnlyAsUniquefor singletons or'OnlyByPropertyfor queryable states.HasMappable: Setting this to'True(as seen onCouponState) enables batch operations on that state type.Aggregate State: TheTreasuryAdaStateis defined with an emptyDatumFieldslist ('[]) and a'NoRefstrategy, 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 = SingleInstanceHere's what this defines:
ManagedStates: Connects each validator to theStateTypes it's responsible for.Params: Defines the data required to parameterize the validator script.ServiceAndPricingValidatoris parameterized by abootstrapUtxoto create a unique instance of the entire service.CustomerValidatoris parameterized by acustomerPkh, 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.