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 Data Types
First, we define standard Haskell records for each piece of on-chain data. Modsefa uses Template Haskell (makeLift and makeIsDataIndexed) to automatically handle the conversion of these types to and from their on-chain Plutus Data representation.
-- The core configuration for the service
data ServiceConfig = ServiceConfig
{ serviceConfigName :: BuiltinByteString
, serviceConfigProvider :: PubKeyHash
} deriving (Eq, Show, Generic)
PlutusTx.makeLift ''ServiceConfig
PlutusTx.makeIsDataIndexed ''ServiceConfig [('ServiceConfig, 0)]
-- A purchasable subscription tier
data PricingTier = PricingTier
{ pricingTierName :: BuiltinByteString
, pricingTierPrice :: Integer
, pricingTierAssetClass :: AssetClass
, pricingTierBillingPeriod:: POSIXTime
, pricingTierContractLength :: POSIXTime
} deriving (Eq, Show, Generic)
PlutusTx.makeLift ''PricingTier
PlutusTx.makeIsDataIndexed ''PricingTier [('PricingTier, 0)]
-- A redeemable coupon for discounts
data Coupon = Coupon
{ couponId :: Integer
, couponBatchId :: TxOutRef
, couponDiscountPercent :: Integer
} deriving (Eq, Show, Generic)
PlutusTx.makeLift ''Coupon
PlutusTx.makeIsDataIndexed ''Coupon [('Coupon, 0)]
instance Mappable Coupon
-- An individual customer's subscription details
data CustomerSubscription = CustomerSubscription
{ customerSubscriptionPkh :: PubKeyHash
, customerSubscriptionPrice :: Integer
, customerSubscriptionAssetClass :: AssetClass
, customerSubscriptionBillingPeriod :: POSIXTime
, customerSubscriptionContractEndDate :: POSIXTime
, customerSubscriptionPaidThrough :: POSIXTime
, customerSubscriptionServiceProviderValidator :: ScriptHash
} deriving (Eq, Show, Generic)
PlutusTx.makeLift ''CustomerSubscription
PlutusTx.makeIsDataIndexed ''CustomerSubscription [('CustomerSubscription, 0)]
-- A placeholder type for our aggregate treasury state
newtype TreasuryAda = TreasuryAda ()
deriving (Show, Eq, Generic)
PlutusTx.makeIsDataIndexed ''TreasuryAda [('TreasuryAda, 0)]
PlutusTx.makeLift ''TreasuryAdaStep 2: Define Your State Types
Next, we promote these data types into StateTypes. This is a simple type alias that links a type-level name (like "ServiceConfig") to the Haskell record (ServiceConfig).
type ServiceConfigState = 'ST "ServiceConfig" ServiceConfig
type PricingTierState = 'ST "PricingTier" PricingTier
type CouponState = 'ST "Coupon" Coupon
type CustomerSubscriptionState = 'ST "CustomerSubscription" CustomerSubscription
type TreasuryAdaState = 'ST "TreasuryAda" TreasuryAdaNow that Modsefa knows about our states, we must implement the StateRepresentable typeclass for each one. This tells the framework how to identify and manage instances of these states on the blockchain.
Instance-Based States:
Most of our states are instance-based, a state instance is identified by a UTxO containing a unique token that is minted by the validator that manages the state (OwnPolicy).
instance StateRepresentable ServiceConfigState where
stateIdentifier _ = TokenIdentified OwnPolicy "ServiceConfig" 1
type AllowedRefStrategy ServiceConfigState = 'OnlyAsUnique
instance StateRepresentable PricingTierState where
stateIdentifier _ = TokenIdentified OwnPolicy "PricingTier" 1
type AllowedRefStrategy PricingTierState = 'OnlyByProperty
instance StateRepresentable CouponState where
stateIdentifier _ = TokenIdentified OwnPolicy "Coupon" 1
type AllowedRefStrategy CouponState = 'OnlyByProperty
instance StateRepresentable CustomerSubscriptionState where
stateIdentifier _ = TokenIdentified OwnPolicy "CustomerSubscription" 1
type AllowedRefStrategy CustomerSubscriptionState = 'OnlyByProperty-
ServiceConfigStatehas a reference strategy of'OnlyAsUnique, which enforces that only one instance of this state can exist for the entire application. -
PricingTierStateandCouponStateuse'OnlyByProperty, allowing for multiple instances that can be queried and referenced by the data in their datums (e.g., "find the pricing tier where the name is 'Premium'").
Aggregate State:
The TreasuryAdaState is different. It's an aggregate state, so its stateIdentifier isn't a unique token but rather the asset it accumulates (GYLovelace). Its reference strategy is 'NoRef' because we don't interact with it as a single instance, but rather as a pool of value.
instance StateRepresentable TreasuryAdaState where
stateIdentifier _ = AggregateAsset GYLovelace
type AllowedRefStrategy TreasuryAdaState = 'NoRefStep 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.