This script will be a parameterised minting policy script that allows the minting of tokens if a certain amount of ADA is paid to the store address as part of the minting transaction.
Writing the validator
We can start by creating the TokenSaleParams that will consist of the store address, the token name, and the token price:
data TokenSaleParams = TokenSaleParams
{
store :: PlutusV2.Address, -- public address of the token store
tName :: PlutusV2.TokenName, -- name of the token to be minted
tPrice :: Integer -- price in ADA per token
}
PlutusTx.unstableMakeIsData ''TokenSaleParams
PlutusTx.makeLift ''TokenSaleParams
The next part is writing the mkPolicy function that will represent our minting logic. We will need quite a bit of helper functions here. First, define the main behaviour of the function which is to use the checkMint function in order to determine whether minting is allowed and write a trace Invalid mint if it is not. Then, we start writing our helper functions for deconstructing info and txOuts from the transaction context.
mkPolicy :: TokenSaleParams -> BuiltinData -> PlutusV2.ScriptContext -> Bool
mkPolicy tsp _ ctx = traceIfFalse "Invalid mint" checkMint
where
info :: PlutusV2.TxInfo
info = PlutusV2.scriptContextTxInfo ctx
txOuts :: [PlutusV2.TxOut]
txOuts = PlutusV2.txInfoOutputs info
We want a checkMint function that will look at the minting field of the transaction (PlutusV2.txInfoMint info) and validate only if there is only one particular token being minted, that token matches our CurrencySymbol and TokenName and the amount minted is valid for the amount of ADA being paid to the store address.
We will call this last variable canMintAmount and it will simply divide (using integer division) the amount of ADA paid to the store (storeTxOutAdaValue) by the price of the token. Getting the storeTxOutAdaValue will be slightly complicated. First, we will need to look at all the UTxOs of the transactions that go to the store address via storeTxOuts :: [PlutusV2.TxOut]. That will give us a list of UTxOs that we then have to filter for their values storeTxOutValue :: PlutusV2.Value and then filter that for only the ADA (Lovelace) values and also sum them all together via storeTxOutLovelaceValue :: Integer. We then simply need to turn that into the corresponding ADA value by dividing it by a million.
To use the underscore format for large numbers to make them more readable such as 1_000_000, we have to activate the extension {-# LANGUAGE NumericUnderscores #-}. Here is the full list of extensions and imports used for this validator for reference:
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE NumericUnderscores #-}
module MintingPolicy
(
scriptSerialised,
writeSerialisedScript,
)
where
import qualified PlutusTx
import PlutusTx.Prelude
import qualified Plutus.V2.Ledger.Api as Plutus
import Cardano.Api.Shelley (PlutusScript (PlutusScriptSerialised), PlutusScriptV2, writeFileTextEnvelope)
import Cardano.Api (FileError)
import qualified Data.ByteString.Lazy as LBS
import qualified Data.ByteString.Short as SBS
import Codec.Serialise
import qualified Ledger.Typed.Scripts as Scripts
import Plutus.Script.Utils.V2.Contexts (ownCurrencySymbol)
import qualified Plutus.V1.Ledger.Value as PlutusV1
import Prelude (IO)
Once our logic is complete, we need to apply our parameter to the function and create a MintingPolicy via PlutusV2.mkMintingPolicyScript. Since we are creating a parameterised script, we need to lift and apply our tsp parameter to our mkPolicy function as we did with our StakingValidator:
To use this MintingPolicy, we need to serialise it as a script first. We can get the Script type via unMintingPolicyScript. We can create and apply the parameter here directly (using the 02.addr as the store address):
Finally, we can load the module in cabal repl from our nix-shell and compile the minting policy.
Prelude> :l src/MintingPolicy.hs
[1 of 1] Compiling MintingPolicy ( src/MintingPolicy.hs, /home/plutus/hpm-plutus/hpm-validators/dist-newstyle/build/x86_64-linux/ghc-8.10.7/hpm-validators-0.1.0.0/build/MintingPolicy.o )
Ok, one module loaded.
Prelude MintingPolicy> writeSerialisedScript
Right ()
Testing the validator
To create the transactions for testing, we need to first get the policy ID from the policy script. From a new directory under testnet/MintingPolicy/ the command looks like this:
Also, cardano-cli accepts only hex token names, so before we can use it as an argument, we need to hexlify our token name via:
echo -n "HPM" | xxd -ps
48504d
Finally, to test this minting policy we can write our usual testing scripts. The check-utxos.sh scripts lists UTxOs available at 01.addr (customer) and 02.addr (store).
We can first test the minting policy by trying to mint an invalid number of tokens for the price. Let's say we want 10 tokens (price 100 ADA), but we only pay 90 ADA in our mint-tokens-invalid.sh script. To create minting transactions, we use the --mint argument which has the following syntax:
--mint VALUE Mint multi-asset value(s) with the multi-asset cli
syntax. You must specify a script witness.
--mint "<TOKEN_QUANTITY> <TOKEN_POLICY_ID>.<TOKEN_NAME>"
If we try running it, we get an Invalid mint error message that we defined in our validator:
./mint-tokens-invalid.sh
Command failed: transaction build Error: The following scripts have execution failures:
the script for policyId 0 (in ascending order of the PolicyIds) failed with:
The Plutus script evaluation failed: An error has occurred: User error:
The machine terminated because of an error, either from a built-in function or from an explicit use of 'error'.
Script debugging logs: Invalid mint
Now, let's try paying the right amount for the 10 tokens in mint-tokens-valid.sh.