Mōdsefa.Donate

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.

  1. generateStateDatum: We call this once for each of our state tags (e.g., ServiceConfigState, PricingTierState, etc.). This splice reads the StateSpec for the tag and generates the corresponding data type.
  2. generateStateInstances: We call this once at the end. It generates all the necessary Plutus instances (ToData, FromData, etc.) and the StateDatum type 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 txId

Notice 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) pricingTiers

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