Mōdsefa.Donate

Defining the User Actions

Tutorial Part 4: Defining the User Actions

Now that we've defined our application's architecture, we can specify what users are allowed to do. For our Feed App, there are two actions: one to create the feed and another to post a new update.

These 'ActionSpec' declarations are the core of our application's logic. From them, Modsefa will generate both the on-chain validator rules and the off-chain code for building transactions.

All the code for this section is in examples/Modsefa/Examples/Feed/Spec.hs.


Action 1: InitializeFeedSpec

The first action brings our Feed App instance to life. It creates the singleton FeedConfigState that defines the feed's properties and the very first FeedDataState entry.

type InitializeFeedSpec =
  'ActionSpec @FeedApp "InitializeFeed"
    '[ 'Op ('Create @FeedConfigState
         '[ 'SetTo "feedName" ('ParamValue "name")
          , 'SetTo "feedOwner" ('ParamValue "owner")
          ]
         '[])
     , 'Op ('Create @FeedDataState
         '[ 'SetTo "feedData" ('ParamValue "content")
          , 'SetTo "feedStatus" ('EnumValue FeedStatus "Active")
          ]
         '[])
     ]
    '[ 'MustSpendValidatorParam "FeedValidator" "bootstrapUtxo"
     ]
    '[ '("name", BuiltinByteString)
     , '("owner", PubKeyHash)
     , '("content", BuiltinByteString)
     ]
  • Operations: The action performs two 'Create' operations:
    • It creates the single FeedConfigState instance, setting the feedName and feedOwner fields from the user-provided action parameters.
    • It creates the first FeedDataState instance, setting its feedData from a parameter and hardcoding its feedStatus to 'EnumValue FeedStatus "Active"'.
  • Constraints: The transaction must satisfy one critical constraint: 'MustSpendValidatorParam "FeedValidator" "bootstrapUtxo"'. This enforces that the transaction spends the unique bootstrapUtxo that parameterizes our FeedValidator. This action "consumes" the bootstrap UTxO to initialize this specific instance of the Feed App, ensuring it can only be done once.
  • Parameters: To execute this action, the user must provide a name for the feed, their own public key hash as the owner, and the initial content.

Action 2: UpdateFeedSpec

This is where the "archive" logic happens. This action posts new content by creating a new FeedDataState instance and simultaneously updating the old "Active" instance to an "Archived" status.

type UpdateFeedSpec =
  'ActionSpec @FeedApp "UpdateFeed"
    '[ 'Op ('Create @FeedDataState
         '[ 'SetTo "feedData" ('ParamValue "newContent")
          , 'SetTo "feedStatus" ('EnumValue FeedStatus "Active")
          ]
         '[])
     , 'Op ('Update @FeedDataState
          ('TypedUniqueWhere ('FieldEquals "feedStatus" ('EnumValue FeedStatus "Active")))
         '[ 'Preserve "feedData"
          , 'SetTo "feedStatus" ('EnumValue FeedStatus "Archived")
          ]
         '[])
     ]
    '[ 'MustBeSignedByState @FeedConfigState 'TypedTheOnlyInstance "feedOwner"
     ]
    '[ '("newContent", BuiltinByteString)
     ]
  • Operations: This action performs a powerful two-part operation:
    1. 'Create @FeedDataState': It first creates a new FeedDataState instance, setting its content to the user's newContent and marking its status as 'Active'.
    2. 'Update @FeedDataState': It then finds the existing state instance that is currently active using the 'TypedUniqueWhere' predicate. It updates this instance by:
      • 'Preserve "feedData"': Enforcing that the content of the old entry remains unchanged.
      • 'SetTo "feedStatus" ('EnumValue FeedStatus "Archived")': Changing its status to 'Archived'.
  • Constraints: 'MustBeSignedByState @FeedConfigState 'TypedTheOnlyInstance "feedOwner"' ensures that only the feedOwner defined in the on-chain FeedConfigState can authorize an update, securing the feed.

This "Create + Update" pattern is how you achieve an immutable, on-chain history. No data is ever erased; it is simply superseded by a new entry.


We have now defined all the user interactions for our Feed App. In the final part, we will see how Modsefa turns these specifications into runnable code and how we interact with the application using the client library.

Defining the User Actions