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 All Validator Scripts

The first step is to generate the on-chain code. Modsefa uses a Template Haskell function, generateAllValidatorsForApp, to do this. You simply provide your top-level AppSpec type, and it will find all associated validators and generate the necessary Plutus Tx code for each one at compile time.

File: examples/Modsefa/Examples/Subscription/Generated.hs

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-} 
 
-- ... imports 
 
-- This single line generates all four validator scripts 
-- (ServiceAndPricingValidator, CouponValidator, CustomerValidator, and TreasuryValidator) 
-- based on the actions defined in SubscriptionApp. $(generateAllValidatorsForApp @SubscriptionApp) 
 
-- This section provides compiled code functions for each validator. 
-- These are used to apply the correct parameters (like the bootstrap UTxO 
-- or a customer's PKH) to the generated scripts. 
 
serviceAndPricingValidatorCompiledCode :: TxOutRef -> CompiledCode (BuiltinData -> BuiltinUnit) 
serviceAndPricingValidatorCompiledCode ref = 
  $$(compile [|| mkWrappedParameterizedServiceAndPricingValidator ||]) 
    `unsafeApplyCode` liftCode plcVersion110 (toBuiltinData ref) 
    
-- ... similar compiled code functions for other validators ...

With that single splice, all the complex on-chain logic—handling different actions, checking constraints, validating state transitions—is generated and guaranteed to be consistent with our specifications.

The generateAllValidatorsForApp splice creates several functions for each validator. The one we interact with directly follows the pattern mkWrappedParameterized<ValidatorAppName>, for example, mkWrappedParameterizedServiceAndPricingValidator. This is the top-level, wrapped validator that is ready to be compiled.


Step 2: Provide Scripts to the Framework

Next, we need to tell Modsefa's transaction builder how to find these compiled scripts. We do this by implementing the AppValidatorScripts typeclass for our application.

File: examples/Modsefa/Examples/Subscription/Scripts.hs

instance AppValidatorScripts SubscriptionApp where 
  getValidatorScript :: forall v. (ValidatorSpec v, Typeable v) =>
    Proxy v -> ParamsToValue (Params v) -> GYScript (ValidatorPlutusVersion v)
  getValidatorScript _ params = 
    case eqT @v @ServiceAndPricingValidator of 
      Just Refl -> scriptFromPlutus $ serviceAndPricingValidatorCompiledCode params 
      Nothing -> case eqT @v @CouponValidator of 
        Just Refl -> scriptFromPlutus $ couponValidatorCompiledCode params 
        Nothing -> case eqT @v @CustomerValidator of 
          Just Refl -> scriptFromPlutus $ customerValidatorCompiledCode params 
          Nothing -> case eqT @v @TreasuryValidator of 
            Just Refl -> scriptFromPlutus $ treasuryValidatorCompiledCode params 
            Nothing -> error "No script implementation for this validator"

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: 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 4: 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.