Extending the basic Plutus app with the constraints API

The previous tutorial (see Writing a basic Plutus app in an emulated environment) showed you how to write a Plutus app that locks some Ada in a script output and splits them evenly between two recipients. In this tutorial, we will reuse the same example, but we will use instead the constraints API which will be used to generate the on-chain and off-chain part of the Plutus app. This will allow your application to create a transaction which is mostly consistent with the validator function.

Given a SplitData, let’s start by defining a function which generates the constraints to unlock funds locked by the split validator.

-- | Create constraints that will be used to spend a locked transaction output
-- from the script address.
-- These constraints will be used in the validation script as well as in the
-- transaction creation step.
{-# INLINABLE splitDataConstraints #-}
splitDataConstraints :: SplitData -> TxConstraints () SplitData
splitDataConstraints SplitData{recipient1, recipient2, amount} =
            Constraints.mustPayToAddress recipient1 (Ada.toValue half)
  `mappend` Constraints.mustPayToAddress recipient2 (Ada.toValue $ amount - half)
     half = Ada.divide amount 2

With the constraints, let’s start by defining the validator function.

-- | The validation logic is generated with `checkScriptContext` based on the set
-- of constraints.
{-# INLINABLE validateSplit #-}
validateSplit :: SplitData -> () -> ScriptContext -> Bool
validateSplit splitData _ =
    Constraints.checkScriptContext (splitDataConstraints splitData)

As you can see, it’s much simpler than the original version.

Now to the off-chain part. The lock endpoint doesn’t change. However, we can change the unlock endpoint to use the constraints we defined above.

unlock :: Promise () SplitSchema T.Text ()
unlock = endpoint @"unlock" (unlockFunds . mkSplitData)

-- | Creates a transaction which spends all script outputs from a script address,
-- sums the value of the scripts outputs and splits it between two payment keys.
unlockFunds :: SplitData -> Contract () SplitSchema T.Text ()
unlockFunds splitData = do
    networkId <- pNetworkId <$> getParams
    -- Get the address of the Split validator
    let contractAddress = Scripts.validatorCardanoAddress networkId splitValidator
    -- Get all utxos that are locked by the Split validator
    utxos <- utxosAt contractAddress
    -- Generate constraints which will spend all utxos locked by the Split
    -- validator and split the value evenly between the two payment keys.
    let constraints = Constraints.spendUtxosFromTheScript utxos ()
                      <> splitDataConstraints splitData
    -- Create, Balance and submit the transaction
    void $ submitTxConstraintsSpending splitValidator utxos constraints

That’s it! The rest of the contract is the same as the previous tutorial.