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 Validator Script
First, we need to generate the on-chain Plutus Tx script for our FeedValidator. We use the generateAllValidatorsForApp Template Haskell function, which analyzes our FeedApp specification and all associated actions to produce the correct validator logic.
File: examples/Modsefa/Examples/Feed/Generated.hs
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
-- ... imports
-- This single line generates the validator script for FeedValidator
$(generateAllValidatorsForApp @FeedApp)
-- This function provides the compiled code for our validator,
-- applying the runtime 'bootstrapUtxo' parameter.
feedValidatorCompiledCode :: TxOutRef -> CompiledCode (BuiltinData -> BuiltinUnit)
feedValidatorCompiledCode ref =
$$(compile [|| mkWrappedParameterizedFeedValidator ||])
`unsafeApplyCode` liftCode plcVersion110 (toBuiltinData ref)A Note on the ...CompiledCode Function
This manual function is necessary because of how Template Haskell operates:
- The
compilefunction ($$) runs at compile-time to generate the Plutus script. - However, our validator needs the
refparameter, which is a value we only have at runtime.
We cannot pass a runtime value to a compile-time function. The feedValidatorCompiledCode function solves this by first compiling the parameterized script and then using unsafeApplyCode to apply the runtime ref when the function is called.
Step 2: Provide the Script to the Framework
Next, we must tell Modsefa's transaction builder how to find and use this compiled script. We do this by implementing the AppValidatorScripts typeclass for our FeedApp.
File: examples/Modsefa/Examples/Feed/Scripts.hs
instance AppValidatorScripts FeedApp where
getValidatorScript :: forall v. (ValidatorSpec v, Typeable v) =>
Proxy v -> ParamsToValue (Params v) -> GYScript (ValidatorPlutusVersion v)
getValidatorScript _ params =
case eqT @v @FeedValidator of
Just Refl -> scriptFromPlutus $ feedValidatorCompiledCode params
Nothing -> error "No script implementation for this validator."Since our app only has one validator, this instance is very simple. It checks if the requested validator v is our FeedValidator, and if so, returns the compiled code with the appropriate parameters applied.
Step 3: 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 4: 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.