Mōdsefa.Donate

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 compile function ($$) runs at compile-time to generate the Plutus script.
  • However, our validator needs the ref parameter, 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 txId

The process is straightforward:

  1. Create an SAppInstance using createAppInstance'.
  2. Load the full ModsefaClient using loadModsefaClient (as shown in the code snippet). This bundles the environment, wallet, and signers.
  3. Define your parameters as a standard Haskell tuple. The ToSParamTuple typeclass (from Modsefa.Client.ActionParams) automatically converts the tuple into the type-safe structure the framework requires.
  4. Call the generic runAction with 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) feedData

This 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:

  1. Define your data and state types.
  2. Specify your validators and application architecture.
  3. Declare your user actions and their constraints.
  4. Generate all the on-chain and off-chain code from that single specification.
  5. 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.