Code Generation and Client Interaction
Tutorial Part 5: Code Generation and Client Interaction
We have now fully specified our Feed App. The magic of Modsefa is that this is all the design work we need to do. From these specifications, the framework will now generate all the complex on-chain and off-chain code for us. In this final part, we will generate the validator, build transactions, and query our application's state.
Step 1: Generate the On-Chain Datum Types
The first generation step is to create the concrete Haskell data types that will be used for our on-chain datums. We do this in a dedicated module (e.g., examples/Modsefa/Examples/Feed/Generated/Datums.hs) by running two Template Haskell splices.
generateStateDatum: We call this once for each of our state tags (FeedConfigState,FeedDataState). This splice reads theStateSpecinstance for each tag and generates the corresponding data type (e.g., dataFeedConfig = ...anddata FeedData = ...).generateStateInstances: We call this once at the end. It generates all the necessary Plutus instances (makeLift,makeIsDataIndexed, etc.) for the types we just created.
-- ... imports
import Modsefa.CodeGen.Generation (generateStateDatum, generateStateInstances)
import Modsefa.Examples.Feed.Types (FeedConfigState, FeedDataState, FeedStatus)
-- Generate datum types
$(generateStateDatum @FeedConfigState)
$(generateStateDatum @FeedDataState)
-- Generate Instances
$(generateStateInstances)Step 2: Generate the Compiled Validator Code
Next, in examples/Modsefa/Examples/Feed/Generated.hs, we generate the raw, compiled Plutus code for our application's validators.
We use a single splice, $(generateAppCode @FeedApp). This splice reads the AppSpec (FeedApp) and generates the underlying parameterized Plutus functions and their compiled CompiledCode representations. For our FeedValidator, it generates the feedValidatorCompiledCode function.
-- ... imports
import Modsefa.CodeGen.Generation (generateAppCode)
import Modsefa.Examples.Feed.Spec (FeedApp)
$(generateAppCode @FeedApp)Step 3: Generate the Validator Script Dispatcher
Finally, in examples/Modsefa/Examples/Feed/Scripts.hs, we create the AppValidatorScripts instance for FeedApp.
The $(generateAppValidatorScripts @FeedApp) splice generates the getValidatorScript function. This function acts as a "dispatcher." It knows how to find the correct compiled code (like feedValidatorCompiledCode from the previous step) for a given validator tag, apply the correct parameters to it, and return a final, usable GYScript.
-- ... imports
import Modsefa.CodeGen.Generation (generateAppValidatorScripts)
import Modsefa.Examples.Feed.Generated (feedValidatorCompiledCode)
import Modsefa.Examples.Feed.Spec (FeedApp)
import Modsefa.Examples.Feed.Validators (FeedValidator)
$(generateAppValidatorScripts @FeedApp)Step 4: Build Transactions with the Client Library
With the setup complete, we can now build transactions. 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 conveniently bundles our connection, wallet, and signing keys into a single ModsefaClient object.
This example shows how a client application would execute our InitializeFeedSpec action.
runInitializeFeed :: String -> Integer -> IO ()
runInitializeFeed txHashStr txIndex = do
-- 1. Define the unique instance of our application
let bootstrapRef = TxOutRef (fromString txHashStr) txIndex
let appInstance = createAppInstance' @FeedApp bootstrapRef
-- 2. Load the ModsefaClient
-- This helper loads "config.json" and "keys/spender.skey"
-- and bundles the environment, wallet, and signers.
client <- loadModsefaClient "config.json" "keys/spender.skey" "feed-init-client"
-- 3. Construct action parameters as a standard Haskell tuple
-- We can get the owner's PKH directly from the loaded client
let (ownerPkh, _) = head (mcSigners client)
let params = ("My First Feed", ownerPkh, "Hello, World!")
-- 4. Call the generic 'runAction'
-- It automatically converts the tuple to the required parameter type
-- and handles all transaction building, balancing, and signing.
result <- runAction
client
appInstance
(Proxy @InitializeFeedSpec)
params
-- 5. Handle the result
case result of
Left err -> putStrLn $ "Transaction failed: " ++ unpack err
Right txId -> putStrLn $ "Transaction submitted! TxId: " ++ show txIdThe process is straightforward:
- Create an
SAppInstanceusingcreateAppInstance'. - Load the full
ModsefaClientusingloadModsefaClient(as shown in the code snippet). This bundles the environment, wallet, and signers. - Define your parameters as a standard Haskell tuple. The
ToSParamTupletypeclass (fromModsefa.Client.ActionParams) automatically converts the tuple into the type-safe structure the framework requires. - Call the generic
runActionwith the client, instance, action proxy, and your parameters tuple.
The runAction function handles all the complexity of building the transaction, signing it, and submitting it to the network, based on the InitializeFeedSpec we defined earlier.
Step 5: Query On-Chain State
Finally, the client library provides the queryStateInstances function to easily find and read the state of our application on the blockchain. We just pass our ClientEnv (from withClientEnv), the SAppInstance we want to query, and a Proxy of the state type we're looking for.
testFeedQuery :: String -> Integer -> IO ()
testFeedQuery txHashStr txIndex = do
-- 1. Define the unique instance of our application
let bootstrapRef = TxOutRef (fromString txHashStr) txIndex
let appInstance = createAppInstance' @FeedApp bootstrapRef
-- 2. Use withClientEnv to get providers/network
withClientEnv "config.json" "feed-query-client" $ \clientEnv -> do
-- 3. Query for all instances of the FeedConfigState
-- The signature is now simpler: just pass the ClientEnv and SAppInstance.
-- No need to create a separate 'queryInstance'.
putStrLn "Querying FeedConfig instances..."
feedConfigs <- queryStateInstances
(Proxy @FeedConfigState)
clientEnv
appInstance
putStrLn $ "Found " ++ show (length feedConfigs) ++ " FeedConfig instances"
mapM_ (print . siData) feedConfigs
-- 4. Query for all instances of the FeedDataState
putStrLn "Querying FeedData instances..."
feedData <- queryStateInstances
(Proxy @FeedDataState)
clientEnv
appInstance
putStrLn $ "Found " ++ show (length feedData) ++ " FeedData instances"
mapM_ (print . siData) feedDataThis function abstracts away the complexity of finding the validator address, scanning for UTxOs with the "FeedConfig" token, and parsing the datums. You simply ask for FeedConfigState instances and get back a list of fully typed Haskell records.
Conclusion
Congratulations! You have successfully built a complete dApp with an automatic on-chain archive.
This tutorial demonstrated the core workflow of Modsefa:
- Define your data and state types.
- Specify your validators and application architecture.
- Declare your user actions and their constraints.
- Generate all the on-chain and off-chain code from that single specification.
- Interact with your dApp using a simple, type-safe client library.
You are now ready to start building your own applications or move on to the more advanced Subscription App tutorial to learn about multi-validator coordination.