Code Generation and Client Interaction
Tutorial Part 6: Code Generation and Client Interaction
So far, we have only written specifications. We haven't written a single line of Plutus Tx validator code or off-chain transaction-building logic. In this final part, we'll see how Modsefa generates all of that for us and how we can use the resulting client library to interact with our dApp.
Step 1: Generate All Validator Scripts
The first step is to generate the on-chain code. Modsefa uses a Template Haskell function, generateAllValidatorsForApp, to do this. You simply provide your top-level AppSpec type, and it will find all associated validators and generate the necessary Plutus Tx code for each one at compile time.
File: examples/Modsefa/Examples/Subscription/Generated.hs
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
-- ... imports
-- This single line generates all four validator scripts
-- (ServiceAndPricingValidator, CouponValidator, CustomerValidator, and TreasuryValidator)
-- based on the actions defined in SubscriptionApp. $(generateAllValidatorsForApp @SubscriptionApp)
-- This section provides compiled code functions for each validator.
-- These are used to apply the correct parameters (like the bootstrap UTxO
-- or a customer's PKH) to the generated scripts.
serviceAndPricingValidatorCompiledCode :: TxOutRef -> CompiledCode (BuiltinData -> BuiltinUnit)
serviceAndPricingValidatorCompiledCode ref =
$$(compile [|| mkWrappedParameterizedServiceAndPricingValidator ||])
`unsafeApplyCode` liftCode plcVersion110 (toBuiltinData ref)
-- ... similar compiled code functions for other validators ...With that single splice, all the complex on-chain logic—handling different actions, checking constraints, validating state transitions—is generated and guaranteed to be consistent with our specifications.
The generateAllValidatorsForApp splice creates several functions for each validator. The one we interact with directly follows the pattern mkWrappedParameterized<ValidatorAppName>, for example, mkWrappedParameterizedServiceAndPricingValidator. This is the top-level, wrapped validator that is ready to be compiled.
Step 2: Provide Scripts to the Framework
Next, we need to tell Modsefa's transaction builder how to find these compiled scripts. We do this by implementing the AppValidatorScripts typeclass for our application.
File: examples/Modsefa/Examples/Subscription/Scripts.hs
instance AppValidatorScripts SubscriptionApp where
getValidatorScript :: forall v. (ValidatorSpec v, Typeable v) =>
Proxy v -> ParamsToValue (Params v) -> GYScript (ValidatorPlutusVersion v)
getValidatorScript _ params =
case eqT @v @ServiceAndPricingValidator of
Just Refl -> scriptFromPlutus $ serviceAndPricingValidatorCompiledCode params
Nothing -> case eqT @v @CouponValidator of
Just Refl -> scriptFromPlutus $ couponValidatorCompiledCode params
Nothing -> case eqT @v @CustomerValidator of
Just Refl -> scriptFromPlutus $ customerValidatorCompiledCode params
Nothing -> case eqT @v @TreasuryValidator of
Just Refl -> scriptFromPlutus $ treasuryValidatorCompiledCode params
Nothing -> error "No script implementation for this validator"This instance is essentially a lookup table. When the transaction builder needs the script for a specific validator (e.g., CouponValidator), it uses this instance to find the correct compiled code function and apply the necessary parameters.
Step 3: Build Transactions with the Client Library
Now for the exciting part, interacting with our application. The client library provides a high-level function, runAction, which handles all the details of building, balancing, signing, and submitting a transaction.
We use it with the loadModsefaClient helper, which bundles our connection, wallet, and signing keys into a single ModsefaClient object. This example shows how a client application would execute the SubscribeSpec action.
runSubscribe :: String -> Integer -> IO ()
runSubscribe txHashStr txIndex = do
-- 1. Define the unique instance of our application
let bootstrapRef = TxOutRef (fromString txHashStr) txIndex
let appInstance = createAppInstance' @SubscriptionApp bootstrapRef
-- 2. Load the ModsefaClient
client <- loadModsefaClient "config.json" "keys/spender.skey" "sub-client"
-- 3. Construct action parameters as a standard Haskell tuple
let (customerPkh, _) = head (mcSigners client)
let tierToSubscribeTo = "Basic Tier" :: BuiltinByteString
let params = (customerPkh, tierToSubscribeTo)
-- 4. Call the generic 'runAction'
result <- runAction
client
appInstance
(Proxy @SubscribeSpec)
params
-- 5. Handle the result
case result of
Left err -> putStrLn $ "Transaction failed: " ++ unpack err
Right txId -> putStrLn $ "Transaction submitted! TxId: " ++ show txIdNotice how simple this is. You don't need to know which validators are involved, how to construct redeemers, or how to calculate payments to the treasury. You simply specify the action and its parameters (as a standard Haskell tuple), and runAction handles the rest based on your AppSpec.
Step 4: Query On-Chain State
Finally, the client library makes it easy to query the current state of your application. The queryStateInstances function allows you to find all instances of a specific StateType.
testSubscriptionQuery :: String -> Integer -> IO ()
testSubscriptionQuery txHashStr txIndex = do
-- 1. Define the unique instance of our application
let bootstrapRef = TxOutRef (fromString txHashStr) txIndex
let appInstance = createAppInstance' @SubscriptionApp bootstrapRef
-- 2. Use withClientEnv to get providers/network
withClientEnv "config.json" "sub-query-client" $ \clientEnv -> do
-- 3. Query for the ServiceConfigState
-- Pass ClientEnv and SAppInstance directly
putStrLn "Querying ServiceConfig instances..."
serviceConfigs <- queryStateInstances
(Proxy @ServiceConfigState)
clientEnv
appInstance
putStrLn $ "Found " ++ show (length serviceConfigs) ++ " ServiceConfig instances"
mapM_ (print . siData) serviceConfigs
-- 4. Query for all PricingTierState instances
putStrLn "Querying PricingTier instances..."
pricingTiers <- queryStateInstances
(Proxy @PricingTierState)
clientEnv
appInstance
putStrLn $ "Found " ++ show (length pricingTiers) ++ " PricingTier instances"
mapM_ (print . siData) pricingTiersThis function abstracts away all the underlying complexity of finding the correct validator address, scanning for UTxOs with the right identification tokens, and parsing the on-chain datums. You simply ask for what you need—ServiceConfigState instances—and you get back a list of strongly-typed Haskell records.
Conclusion
Congratulations! You have now walked through a complete, multi-validator dApp built with Modsefa.
We started by defining our data types and ended with a fully functional client for building transactions and querying state. Crucially, the complex and error-prone parts of the process—the on-chain validation logic and the off-chain transaction building—were completely generated from a single, declarative specification.
This is the power of specification-driven development with Modsefa: you focus on your application's business logic, and the framework guarantees that your on-chain and off-chain code are, and always will be, perfectly in sync.