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 the On-Chain Datum Types
The first generation step creates the concrete Haskell data types for our on-chain datums. This is done in a dedicated module, examples/Modsefa/Examples/Subscription/Generated/Datums.hs.
generateStateDatum: We call this once for each of our state tags (e.g.,ServiceConfigState,PricingTierState, etc.). This splice reads theStateSpecfor the tag and generates the correspondingdatatype.generateStateInstances: We call this once at the end. It generates all the necessary Plutus instances (ToData,FromData, etc.) and theStateDatumtype family instance for all the types we just created.
File: examples/Modsefa/Examples/Subscription/Generated/Datums.hs
-- ... imports
import Modsefa.CodeGen.Generation (generateStateDatum, generateStateInstances)
import Modsefa.Examples.Subscription.Types
( CouponState, CustomerSubscriptionState, PricingTierState , ServiceConfigState
, TreasuryAdaState
)
-- Generate all the data types
$(generateStateDatum @ServiceConfigState)
$(generateStateDatum @PricingTierState)
$(generateStateDatum @CouponState)
$(generateStateDatum @CustomerSubscriptionState)
$(generateStateDatum @TreasuryAdaState)
-- Generate all the instances
$(generateStateInstances)Step 2: Generate the Compiled Validator Code
Next, in examples/Modsefa/Examples/Subscription/Generated.hs, we generate the raw, compiled Plutus code for our application's validators.
We use a single splice, $(generateAppCode @SubscriptionApp). This reads our AppSpec and generates the underlying parameterized Plutus functions and their CompiledCode representations (e.g., serviceAndPricingValidatorCompiledCode, couponValidatorCompiledCode, etc.).
File: examples/Modsefa/Examples/Subscription/Generated.hs
-- ... imports
import Modsefa.CodeGen.Generation (generateAppCode)
import Modsefa.Examples.Subscription.Spec (SubscriptionApp)
-- This one splice generates all compiled code functions
$(generateAppCode @SubscriptionApp)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: Generate the Validator Script Dispatcher
Finally, in examples/Modsefa/Examples/Subscription/Scripts.hs, we create the AppValidatorScripts instance for SubscriptionApp.
We use another single splice, $(generateAppValidatorScripts @SubscriptionApp). This generates the getValidatorScript function, which acts as a "dispatcher." It knows how to find the correct compiled code from the previous step (like couponValidatorCompiledCode) for a given validator tag, apply the correct parameters to it, and return a final, usable GYScript.
File: examples/Modsefa/Examples/Subscription/Scripts.hs
-- ... imports
import Modsefa.CodeGen.Generation (generateAppValidatorScripts)
import Modsefa.Examples.Subscription.Generated
( couponValidatorCompiledCode, customerValidatorCompiledCode
, serviceAndPricingValidatorCompiledCode, treasuryValidatorCompiledCode
)
import Modsefa.Examples.Subscription.Spec (SubscriptionApp)
import Modsefa.Examples.Subscription.Validators
( CouponValidator, CustomerValidator, ServiceAndPricingValidator
, TreasuryValidator
)
-- This splice generates the AppValidatorScripts instance
$(generateAppValidatorScripts @SubscriptionApp)Step 4: 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 5: 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.